import os import shutil import curses from collections import OrderedDict from contextlib import contextmanager from tempfile import mkdtemp, NamedTemporaryFile import pytest from rtv.theme import Theme from rtv.config import DEFAULT_THEMES from rtv.exceptions import ConfigError try: from unittest import mock except ImportError: import mock INVALID_ELEMENTS = OrderedDict([ ('too_few_items', 'Upvote = blue\n'), ('too_many_items', 'Upvote = blue blue bold underline\n'), ('invalid_fg', 'Upvote = invalid blue\n'), ('invalid_bg', 'Upvote = blue invalid\n'), ('invalid_attr', 'Upvote = blue blue bold+invalid\n'), ('invalid_hex', 'Upvote = #fffff blue\n'), ('invalid_hex2', 'Upvote = #gggggg blue\n'), ('out_of_range', 'Upvote = ansi_256 blue\n'), ('something_invalid', 'non_existing_key_without_value\n') ]) @contextmanager def _ephemeral_directory(): # All of the temporary files for the theme tests must # be initialized in separate directories, so the tests # can run in parallel without accidentally loading theme # files from each other dirname = None try: dirname = mkdtemp() yield dirname finally: if dirname: shutil.rmtree(dirname, ignore_errors=True) def test_theme_invalid_source(): with pytest.raises(ValueError): Theme(name='default', source=None) with pytest.raises(ValueError): Theme(name=None, source='installed') def test_theme_default_construct(): theme = Theme() assert theme.name == 'default' assert theme.source == 'built-in' assert theme.required_colors == 8 assert theme.required_color_pairs == 6 for fg, bg, attr in theme.elements.values(): assert isinstance(fg, int) assert isinstance(bg, int) assert isinstance(attr, int) def test_theme_monochrome_construct(): theme = Theme(use_color=False) assert theme.name == 'monochrome' assert theme.source == 'built-in' assert theme.required_colors == 0 assert theme.required_color_pairs == 0 def test_theme_256_construct(): elements = {'CursorBar1': (None, 101, curses.A_UNDERLINE)} theme = Theme(elements=elements) assert theme.elements['CursorBar1'] == (-1, 101, curses.A_UNDERLINE) assert theme.required_colors == 256 def test_theme_element_selected_attributes(): elements = { 'Normal': (1, 2, curses.A_REVERSE), 'Selected': (2, 3, None), 'TitleBar': (4, None, curses.A_BOLD), 'Link': (5, None, None)} theme = Theme(elements=elements) assert theme.elements['Normal'] == (1, 2, curses.A_REVERSE) # All of the normal elements fallback to the attributes of "Normal" assert theme.elements['Selected'] == (2, 3, curses.A_REVERSE) assert theme.elements['TitleBar'] == (4, 2, curses.A_BOLD) assert theme.elements['Link'] == (5, 2, curses.A_REVERSE) # The @Selected mode will overwrite any other attributes with # the ones defined in "Selected". Because "Selected" defines # a foreground and a background color, they will override the # ones that "Link" had defined. # assert theme.elements['@Link'] == (2, 3, curses.A_REVERSE) # I can't remember why the above rule was implemented, so I reverted it assert theme.elements['@Link'] == (5, 3, curses.A_REVERSE) assert '@Normal' not in theme.elements assert '@Selected' not in theme.elements assert '@TitleBar' not in theme.elements def test_theme_default_cfg_matches_builtin(): filename = os.path.join(DEFAULT_THEMES, 'default.cfg.example') default_theme = Theme.from_file(filename, 'built-in') # The default theme file should match the hardcoded values assert default_theme.elements == Theme().elements # Make sure that the elements passed into the constructor exactly match # up with the hardcoded elements class MockTheme(Theme): def __init__(self, name=None, source=None, elements=None): assert name == 'default.cfg' assert source == 'preset' assert elements == Theme.DEFAULT_ELEMENTS MockTheme.from_file(filename, 'preset') args, ids = INVALID_ELEMENTS.values(), list(INVALID_ELEMENTS) @pytest.mark.parametrize('line', args, ids=ids) def test_theme_from_file_invalid(line): with _ephemeral_directory() as dirname: with NamedTemporaryFile(mode='w+', dir=dirname) as fp: fp.write('[theme]\n') fp.write(line) fp.flush() with pytest.raises(ConfigError): Theme.from_file(fp.name, 'installed') def test_theme_from_file(): with _ephemeral_directory() as dirname: with NamedTemporaryFile(mode='w+', dir=dirname) as fp: with pytest.raises(ConfigError): Theme.from_file(fp.name, 'installed') fp.write('[theme]\n') fp.write('Unknown = - -\n') fp.write('Upvote = - red\n') fp.write('Downvote = ansi_255 default bold\n') fp.write('NeutralVote = #000000 #ffffff bold+reverse\n') fp.flush() theme = Theme.from_file(fp.name, 'installed') assert theme.source == 'installed' assert 'Unknown' not in theme.elements assert theme.elements['Upvote'] == ( -1, curses.COLOR_RED, curses.A_NORMAL) assert theme.elements['Downvote'] == ( 255, -1, curses.A_BOLD) assert theme.elements['NeutralVote'] == ( 16, 231, curses.A_BOLD | curses.A_REVERSE) def test_theme_from_name(): with _ephemeral_directory() as dirname: with NamedTemporaryFile(mode='w+', suffix='.cfg', dir=dirname) as fp: path, filename = os.path.split(fp.name) theme_name = filename[:-4] fp.write('[theme]\n') fp.write('Upvote = default default\n') fp.flush() # Full file path theme = Theme.from_name(fp.name, path=path) assert theme.name == theme_name assert theme.source == 'custom' assert theme.elements['Upvote'] == (-1, -1, curses.A_NORMAL) # Relative to the directory theme = Theme.from_name(theme_name, path=path) assert theme.name == theme_name assert theme.source == 'installed' assert theme.elements['Upvote'] == (-1, -1, curses.A_NORMAL) # Invalid theme name with pytest.raises(ConfigError): theme.from_name('invalid_theme_name', path=path) def test_theme_initialize_attributes(stdscr): theme = Theme() with pytest.raises(RuntimeError): theme.get('Upvote') theme.bind_curses() assert len(theme._color_pair_map) == theme.required_color_pairs for element in Theme.DEFAULT_ELEMENTS: assert isinstance(theme.get(element), int) theme = Theme(use_color=False) theme.bind_curses() def test_theme_initialize_attributes_monochrome(stdscr): theme = Theme(use_color=False) theme.bind_curses() theme.get('Upvote') # Avoid making these curses calls if colors aren't initialized assert not curses.init_pair.called assert not curses.color_pair.called def test_theme_list_themes(): with _ephemeral_directory() as dirname: with NamedTemporaryFile(mode='w+', suffix='.cfg', dir=dirname) as fp: path, filename = os.path.split(fp.name) theme_name = filename[:-4] fp.write('[theme]\n') fp.flush() Theme.print_themes(path) themes, errors = Theme.list_themes(path) assert not errors theme_strings = [t.display_string for t in themes] assert theme_name + ' (installed)' in theme_strings assert 'default (built-in)' in theme_strings assert 'monochrome (built-in)' in theme_strings assert 'molokai (preset)' in theme_strings def test_theme_list_themes_invalid(): with _ephemeral_directory() as dirname: with NamedTemporaryFile(mode='w+', suffix='.cfg', dir=dirname) as fp: path, filename = os.path.split(fp.name) theme_name = filename[:-4] fp.write('[theme]\n') fp.write('Upvote = invalid value\n') fp.flush() Theme.print_themes(path) themes, errors = Theme.list_themes(path) assert ('installed', theme_name) in errors def test_theme_presets_define_all_elements(): # The themes in the preset themes/ folder should have all of the valid # elements defined in their configuration. class MockTheme(Theme): def __init__(self, name=None, source=None, elements=None, use_color=True): if source == 'preset': assert set(elements.keys()) == set(Theme.DEFAULT_ELEMENTS.keys()) super(MockTheme, self).__init__(name, source, elements, use_color) themes, errors = MockTheme.list_themes() assert sum([theme.source == 'preset' for theme in themes]) >= 4