Repository: zulip/zulip-terminal (python)

Issue:
Ensure color codes are valid at runtime if running in 16-color mode
Zulip terminal runs in 16 color depth mode only if the color codes used in themes for the same are acceptable by urwid. Codes of those 16 acceptable colors are mentioned in the [documentation](https://github.com/zulip/zulip-terminal/blob/main/zulipterminal/themes/THEME_CONTRIBUTING.md). 
In case the contributor uses some other color code instead of the aforementioned codes, the terminal shall not run. The prompted error in such a case, (as of now) doesn't indicate the exact reason for the error. 
Construct a test in the codebase to check if the 16 color codes used are acceptable ones and prompt an error otherwise. 
PS: This issue is a further development of PR #1154.
Related discussions on CZO: https://chat.zulip.org/#narrow/stream/206-zulip-terminal/topic/16-color.20names.20in.20themes

Pull request:
Ensure color codes are valid at runtime if running in 16-color mode.
<!-- Please see https://github.com/zulip/zulip-terminal#contributor-guidelines ! -->

**What does this PR do?**  <!-- Overall description goes here -->
In ``/zulip-terminal/zulipterminal/config/themes.py`` code is added in ``generate_theme`` function to check if valid color codes are used in color files for specific themes. For eg. (``colors_gruvbox.py`` for ``gruvbox_dark.py`` theme)

<!-- If fixing a filed bug or new feature, add 'Fixes #<issue>' or 'Partial fix for #<issue>' -->
Fixes #1158 

<!-- Add a link to a discussion on chat.zulip.org, if relevant -->

**Tested?** <!-- Fine to leave some of these unchecked if this is a draft/work-in-progress -->
- [x] Manually
- [x] Existing tests (adapted, if necessary)
- [x] New tests added (for any new behavior)
- [x] Passed linting & tests (each commit)
<!-- Code must pass CI (GitHub Actions) before merging - look for the green tick! -->

<!-- See https://github.com/zulip/zulip-terminal#commit-style -->
**Commit flow** <!-- if more than one commit; add/delete/fill-in as appropriate -->
<!-- For example:
- first commit doing some thing
- maybe multiple commits doing similar things
-->

**Notes & Questions** <!-- if any; add/delete/fill-in as appropriate -->
<!-- For example:
- this doesn't include feature X (yet?)
- unsure about Y
- should this do Z?
-->

**Interactions** <!-- if any; add/delete/fill-in as appropriate -->
<!-- eg.
- Waiting on #<PR>
- Blocks #<PR>
-->

**Visual changes** <!-- if any; add/delete/fill-in with screenshot/diagram as appropriate -->


Patch:
diff --git a/zulipterminal/cli/run.py b/zulipterminal/cli/run.py
index 6b50f92..12425cf 100755
--- a/zulipterminal/cli/run.py
+++ b/zulipterminal/cli/run.py
@@ -12,6 +12,7 @@ import requests
 from urwid import display_common, set_encoding
 
 from zulipterminal.config.themes import (
+    InvalidThemeColorCode,
     aliased_themes,
     all_themes,
     complete_and_incomplete_themes,
@@ -511,6 +512,11 @@ def main(options: Optional[List[str]] = None) -> None:
         zt_logger.info(f"\n\n{e}\n\n")
         zt_logger.exception(e)
         exit_with_error(f"\nError connecting to Zulip server: {e}.")
+    except InvalidThemeColorCode as e:
+        # Acts as separator between logs
+        zt_logger.info(f"\n\n{e}\n\n")
+        zt_logger.exception(e)
+        exit_with_error(f"\n{e}")
     except (display_common.AttrSpecError, display_common.ScreenError) as e:
         # NOTE: Strictly this is not necessarily just a theme error
         # FIXME: Add test for this - once loading takes place after UI setup
diff --git a/zulipterminal/config/themes.py b/zulipterminal/config/themes.py
index c6cfdab..6eb7e87 100644
--- a/zulipterminal/config/themes.py
+++ b/zulipterminal/config/themes.py
@@ -103,6 +103,31 @@ THEME_ALIASES = {
     "blue": "zt_blue",
 }
 
+# These are urwid color names with underscores instead of spaces
+valid_16_color_codes = [
+    "default",
+    "black",
+    "dark_red",
+    "dark_green",
+    "brown",
+    "dark_blue",
+    "dark_magenta",
+    "dark_cyan",
+    "dark_gray",
+    "light_red",
+    "light_green",
+    "yellow",
+    "light_blue",
+    "light_magenta",
+    "light_cyan",
+    "light_gray",
+    "white",
+]
+
+
+class InvalidThemeColorCode(Exception):
+    pass
+
 
 def all_themes() -> List[str]:
     return list(THEMES.keys())
@@ -127,6 +152,7 @@ def complete_and_incomplete_themes() -> Tuple[List[str], List[str]]:
 
 def generate_theme(theme_name: str, color_depth: int) -> ThemeSpec:
     theme_styles = THEMES[theme_name].STYLES
+    validate_colors(theme_name, color_depth)
     urwid_theme = parse_themefile(theme_styles, color_depth)
 
     try:
@@ -138,6 +164,30 @@ def generate_theme(theme_name: str, color_depth: int) -> ThemeSpec:
     return urwid_theme
 
 
+def validate_colors(theme_name: str, color_depth: int) -> None:
+    """
+    This function validates color-codes for a given theme, given colors are in `Color`.
+
+    If any color is not in accordance with urwid default 16-color codes then the
+    function raises InvalidThemeColorCode with the invalid colors.
+    """
+    theme_colors = THEMES[theme_name].Color
+    failure_text = []
+    if color_depth == 16:
+        for color in theme_colors:
+            color_16code = color.value.split()[0]
+            if color_16code not in valid_16_color_codes:
+                invalid_16_color_code = str(color.name)
+                failure_text.append(f"- {invalid_16_color_code} = {color_16code}")
+        if failure_text == []:
+            return
+        else:
+            text = "\n".join(
+                [f"Invalid 16-color codes in theme '{theme_name}':"] + failure_text
+            )
+            raise InvalidThemeColorCode(text)
+
+
 def parse_themefile(
     theme_styles: Dict[Optional[str], Tuple[Any, Any]], color_depth: int
 ) -> ThemeSpec:


Test patch:
diff --git a/tests/config/test_themes.py b/tests/config/test_themes.py
index 29edef7..59827df 100644
--- a/tests/config/test_themes.py
+++ b/tests/config/test_themes.py
@@ -11,11 +11,14 @@ from zulipterminal.config.regexes import REGEX_COLOR_VALID_FORMATS
 from zulipterminal.config.themes import (
     REQUIRED_STYLES,
     THEMES,
+    InvalidThemeColorCode,
     ThemeSpec,
     add_pygments_style,
     all_themes,
     complete_and_incomplete_themes,
     parse_themefile,
+    valid_16_color_codes,
+    validate_colors,
 )
 
 
@@ -27,27 +30,6 @@ expected_complete_themes = {
     "zt_blue",
 }
 
-# These are urwid color names with underscores instead of spaces
-valid_16_color_codes = [
-    "default",
-    "black",
-    "dark_red",
-    "dark_green",
-    "brown",
-    "dark_blue",
-    "dark_magenta",
-    "dark_cyan",
-    "dark_gray",
-    "light_red",
-    "light_green",
-    "yellow",
-    "light_blue",
-    "light_magenta",
-    "light_cyan",
-    "light_gray",
-    "white",
-]
-
 
 def test_all_themes() -> None:
     assert all_themes() == list(THEMES.keys())
@@ -213,3 +195,79 @@ def test_add_pygments_style(
     # Check for overrides(k,sd) and inheriting styles (kr)
     for style in expected_styles:
         assert style in urwid_theme
+
+
+# Validate 16-color-codes
+@pytest.mark.parametrize(
+    "color_depth, theme_name",
+    [
+        (16, "zt_dark"),
+        (16, "gruvbox_dark"),
+        (16, "gruvbox_light"),
+        (16, "zt_light"),
+        (16, "zt_blue"),
+    ],
+)
+def test_validate_colors(theme_name: str, color_depth: int) -> None:
+    theme = THEMES[theme_name]
+
+    header_text = f"Invalid 16-color codes in theme '{theme_name}':\n"
+
+    # No invalid colors
+    class Color(Enum):
+        # color          =  16code          256code   24code
+        DEFAULT = "default         default   default"
+        DARK0_HARD = "black           h234      #1d2021"
+        GRAY_244 = "dark_gray       h244      #928374"
+        LIGHT2 = "white           h250      #d5c4a1"
+
+    theme.Color = Color
+    validate_colors(theme_name, 16)
+
+    # One invalid color
+    class Color1(Enum):
+        # color          =  16code          256code   24code
+        DEFAULT = "default         default   default"
+        DARK0_HARD = "blac           h234      #1d2021"
+        GRAY_244 = "dark_gray       h244      #928374"
+        LIGHT2 = "white           h250      #d5c4a1"
+
+    theme.Color = Color1
+    with pytest.raises(InvalidThemeColorCode) as e:
+        validate_colors(theme_name, 16)
+    assert str(e.value) == header_text + "- DARK0_HARD = blac"
+
+    # Two invalid colors
+    class Color2(Enum):
+        # color          =  16code          256code   24code
+        DEFAULT = "default         default   default"
+        DARK0_HARD = "blac           h234      #1d2021"
+        GRAY_244 = "dark_gra       h244      #928374"
+        LIGHT2 = "white           h250      #d5c4a1"
+
+    theme.Color = Color2
+    with pytest.raises(InvalidThemeColorCode) as e:
+        validate_colors(theme_name, 16)
+    assert (
+        str(e.value) == header_text + "- DARK0_HARD = blac\n" + "- GRAY_244 = dark_gra"
+    )
+
+    # Multiple invalid colors
+    class Color3(Enum):
+        # color          =  16code          256code   24code
+        DEFAULT = "defaul         default   default"
+        DARK0_HARD = "blac           h234      #1d2021"
+        GRAY_244 = "dark_gra       h244      #928374"
+        LIGHT2 = "whit           h250      #d5c4a1"
+
+    theme.Color = Color3
+    with pytest.raises(InvalidThemeColorCode) as e:
+        validate_colors(theme_name, 16)
+    assert (
+        str(e.value)
+        == header_text
+        + "- DEFAULT = defaul\n"
+        + "- DARK0_HARD = blac\n"
+        + "- GRAY_244 = dark_gra\n"
+        + "- LIGHT2 = whit"
+    )