diff --git a/cloudsmith_cli/core/tests/test_terminal_color.py b/cloudsmith_cli/core/tests/test_terminal_color.py new file mode 100644 index 00000000..f1c9889c --- /dev/null +++ b/cloudsmith_cli/core/tests/test_terminal_color.py @@ -0,0 +1,123 @@ +from enum import Flag, auto +from typing import ClassVar + +import pytest + +from cloudsmith_cli.core.utils import ( + ColorMode, + TTYMode, + color_enabled, + is_interactive, +) + + +class Desired(Flag): + NONE = 0 + COLOR = auto() + INTERACTIVE = auto() + + +class TestTerminalUISuppression: + """Testing precedence in environment variables for controlling interactivity and colour""" + + env_tests: ClassVar[list] = [ + pytest.param( + {}, + ColorMode.AUTO, + TTYMode.ENABLED, + Desired.COLOR | Desired.INTERACTIVE, + id="default behaviour", + ), + pytest.param( + {}, + ColorMode.NEVER, + TTYMode.DISABLED, + Desired.NONE, + id="force disable tty and color mode", + ), + pytest.param( + {"NO_COLOR": ""}, + ColorMode.AUTO, + TTYMode.ENABLED, + Desired.COLOR | Desired.INTERACTIVE, + id="empty no color var", + ), + pytest.param( + {"CLOUDSMITH_FORCE_COLOR": "true"}, + ColorMode.AUTO, + TTYMode.ENABLED, + Desired.COLOR | Desired.INTERACTIVE, + id="force ANSI output", + ), + pytest.param( + {"NO_COLOR": "true", "CLOUDSMITH_FORCE_COLOR": "true"}, + ColorMode.AUTO, + TTYMode.ENABLED, + Desired.INTERACTIVE, + id="force ANSI output, respects NO_COLOR", + ), + pytest.param( + {"TERM": "dumb"}, + ColorMode.AUTO, + TTYMode.ENABLED, + Desired.INTERACTIVE, + id="no color enabled for TERM=dumb", + ), + pytest.param( + {"CI": "true"}, + ColorMode.AUTO, + TTYMode.ENABLED, + Desired.COLOR, + id="in a CI server, do not use interactive features", + ), + pytest.param( + {"CI": "true", "NO_COLOR": "true"}, + ColorMode.AUTO, + TTYMode.ENABLED, + Desired.NONE, + id="suppress interactivity and colour", + ), + pytest.param( + { + "NO_COLOR": "true", + "TERM": "dumb", + "CLOUDSMITH_FORCE_COLOR": "true", + }, + ColorMode.AUTO, + TTYMode.ENABLED, + Desired.INTERACTIVE, + id="test NO_COLOR always respected", + ), + pytest.param( + { + "TERM": "dumb", + "CLOUDSMITH_FORCE_COLOR": "true", + }, + ColorMode.AUTO, + TTYMode.ENABLED, + Desired.COLOR | Desired.INTERACTIVE, + id="ensure force tty respected over term dumb", + ), + ] + + @pytest.mark.parametrize("env,colorMode,ttyMode,desired", env_tests) + def test_no_color_environment_variables( + self, + env: dict[str, str], + colorMode: ColorMode, + ttyMode: TTYMode, + desired: Desired, + ): + want_color = Desired.COLOR in desired + assert color_enabled(env, colorMode, ttyMode) == want_color, ( + f"colour suppression check failed for environment: {env} wanted {desired}" + ) + + @pytest.mark.parametrize("env,colorMode,ttyMode,desired", env_tests) + def test_no_interactive_environment_variables( + self, env, colorMode, ttyMode, desired + ): + want_interactive = Desired.INTERACTIVE in desired + assert is_interactive(env, ttyMode) == want_interactive, ( + f"interactive suppression check failed for environment {env} wanted {desired}" + ) diff --git a/cloudsmith_cli/core/utils.py b/cloudsmith_cli/core/utils.py index 6d863910..c3fca8c9 100644 --- a/cloudsmith_cli/core/utils.py +++ b/cloudsmith_cli/core/utils.py @@ -2,6 +2,7 @@ import hashlib import os +from enum import Enum, auto import click @@ -11,6 +12,55 @@ def get_help_website(): return "https://docs.cloudsmith.com/developer-tools/cli" +class ColorMode(Enum): + AUTO = auto() + ALWAYS = auto() + NEVER = auto() + + +class TTYMode(Enum): + ENABLED = auto() + DISABLED = auto() + + +def color_enabled(env: dict[str, str], colorMode: ColorMode, ttyMode: TTYMode) -> bool: + """Suppresses ANSI colour based on environment and CLI input""" + match colorMode: + case ColorMode.ALWAYS: + return True + case ColorMode.NEVER: + return False + case ColorMode.AUTO: + pass + + if env.get("NO_COLOR"): + return False + if env.get("CLOUDSMITH_FORCE_COLOR") == "true": + return True + if env.get("TERM") == "dumb": + return False + + match ttyMode: + case TTYMode.ENABLED: + return True + case TTYMode.DISABLED: + return False + return True + + +def is_interactive(env: dict[str, str], tty_mode: TTYMode) -> bool: + """Suppresses interactive prompts for user input based on conditions in the shell environment""" + if env.get("CI"): + return False + + match tty_mode: + case TTYMode.ENABLED: + return True + case TTYMode.DISABLED: + return False + return True + + def get_github_website(): """Get the URL for the GitHub project.""" return "https://github.com/cloudsmith-io/cloudsmith-cli"