diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2022-09-16 09:10:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2022-09-16 09:10:14 +0000 |
commit | 896739353a613f23c007d9acaa2809010a522a37 (patch) | |
tree | cadd194400c11d0a5caaeda7d9d771602eb1ba40 /tests | |
parent | Initial commit. (diff) | |
download | colorclass-896739353a613f23c007d9acaa2809010a522a37.tar.xz colorclass-896739353a613f23c007d9acaa2809010a522a37.zip |
Adding upstream version 2.2.0.upstream/2.2.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests')
-rw-r--r-- | tests/__init__.py | 1 | ||||
-rw-r--r-- | tests/conftest.py | 78 | ||||
-rw-r--r-- | tests/screenshot.py | 299 | ||||
-rw-r--r-- | tests/sub_box_green_win10.bmp | bin | 0 -> 5026 bytes | |||
-rw-r--r-- | tests/sub_box_green_winxp.bmp | bin | 0 -> 7254 bytes | |||
-rw-r--r-- | tests/sub_box_sans_win10.bmp | bin | 0 -> 5026 bytes | |||
-rw-r--r-- | tests/sub_box_sans_winxp.bmp | bin | 0 -> 7254 bytes | |||
-rw-r--r-- | tests/sub_red_dark_fg_win10.bmp | bin | 0 -> 446 bytes | |||
-rw-r--r-- | tests/sub_red_dark_fg_winxp.bmp | bin | 0 -> 702 bytes | |||
-rw-r--r-- | tests/sub_red_light_fg_win10.bmp | bin | 0 -> 418 bytes | |||
-rw-r--r-- | tests/sub_red_light_fg_winxp.bmp | bin | 0 -> 702 bytes | |||
-rw-r--r-- | tests/sub_red_sans_win10.bmp | bin | 0 -> 446 bytes | |||
-rw-r--r-- | tests/sub_red_sans_winxp.bmp | bin | 0 -> 882 bytes | |||
-rw-r--r-- | tests/test___main__.py | 64 | ||||
-rw-r--r-- | tests/test_codes.py | 137 | ||||
-rw-r--r-- | tests/test_color.py | 185 | ||||
-rw-r--r-- | tests/test_core.py | 398 | ||||
-rw-r--r-- | tests/test_example.py | 96 | ||||
-rw-r--r-- | tests/test_parse.py | 79 | ||||
-rw-r--r-- | tests/test_search.py | 51 | ||||
-rw-r--r-- | tests/test_toggles.py | 29 | ||||
-rw-r--r-- | tests/test_windows.py | 429 |
22 files changed, 1846 insertions, 0 deletions
diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..bf16d1e --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Allows importing from conftest.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5f404db --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,78 @@ +"""Configure tests.""" + +import py +import pytest + +from colorclass.codes import ANSICodeMapping +from colorclass.color import Color +from colorclass.core import ColorStr, PARENT_CLASS + +PROJECT_ROOT = py.path.local(__file__).dirpath().join('..') + + +@pytest.fixture(autouse=True) +def set_defaults(monkeypatch): + """Set ANSICodeMapping defaults before each test. + + :param monkeypatch: pytest fixture. + """ + monkeypatch.setattr(ANSICodeMapping, 'DISABLE_COLORS', False) + monkeypatch.setattr(ANSICodeMapping, 'LIGHT_BACKGROUND', False) + + +def assert_both_values(actual, expected_plain, expected_color, kind=None): + """Handle asserts for color and non-color strings in color and non-color tests. + + :param ColorStr actual: Return value of ColorStr class method. + :param expected_plain: Expected non-color value. + :param expected_color: Expected color value. + :param str kind: Type of string to test. + """ + if kind.endswith('plain'): + assert actual.value_colors == expected_plain + assert actual.value_no_colors == expected_plain + assert actual.has_colors is False + elif kind.endswith('color'): + assert actual.value_colors == expected_color + assert actual.value_no_colors == expected_plain + if '\033' in actual.value_colors: + assert actual.has_colors is True + else: + assert actual.has_colors is False + else: + assert actual == expected_plain + + if kind.startswith('ColorStr'): + assert actual.__class__ == ColorStr + elif kind.startswith('Color'): + assert actual.__class__ == Color + + +def get_instance(kind, sample=None, color='red'): + """Get either a string, non-color ColorStr, or color ColorStr instance. + + :param str kind: Type of string to test. + :param iter sample: Input test to derive instances from. + :param str color: Color tags to use. Default is red. + + :return: Instance. + """ + # First determine which class/type to use. + if kind.startswith('ColorStr'): + cls = ColorStr + elif kind.startswith('Color'): + cls = Color + else: + cls = PARENT_CLASS + + # Next handle NoneType samples. + if sample is None: + return cls() + + # Finally handle non-None samples. + if kind.endswith('plain'): + return cls(sample) + elif kind.endswith('color'): + tags = '{%s}' % color, '{/%s}' % color + return cls(tags[0] + sample + tags[1]) + return sample diff --git a/tests/screenshot.py b/tests/screenshot.py new file mode 100644 index 0000000..cc391eb --- /dev/null +++ b/tests/screenshot.py @@ -0,0 +1,299 @@ +"""Take screenshots and search for subimages in images.""" + +import ctypes +import os +import random +import struct +import subprocess +import time + +try: + from itertools import izip +except ImportError: + izip = zip # Py3 + +from colorclass.windows import WINDOWS_CODES +from tests.conftest import PROJECT_ROOT + +STARTF_USEFILLATTRIBUTE = 0x00000010 +STARTF_USESHOWWINDOW = getattr(subprocess, 'STARTF_USESHOWWINDOW', 1) +STILL_ACTIVE = 259 +SW_MAXIMIZE = 3 + + +class StartupInfo(ctypes.Structure): + """STARTUPINFO structure.""" + + _fields_ = [ + ('cb', ctypes.c_ulong), + ('lpReserved', ctypes.c_char_p), + ('lpDesktop', ctypes.c_char_p), + ('lpTitle', ctypes.c_char_p), + ('dwX', ctypes.c_ulong), + ('dwY', ctypes.c_ulong), + ('dwXSize', ctypes.c_ulong), + ('dwYSize', ctypes.c_ulong), + ('dwXCountChars', ctypes.c_ulong), + ('dwYCountChars', ctypes.c_ulong), + ('dwFillAttribute', ctypes.c_ulong), + ('dwFlags', ctypes.c_ulong), + ('wShowWindow', ctypes.c_ushort), + ('cbReserved2', ctypes.c_ushort), + ('lpReserved2', ctypes.c_char_p), + ('hStdInput', ctypes.c_ulong), + ('hStdOutput', ctypes.c_ulong), + ('hStdError', ctypes.c_ulong), + ] + + def __init__(self, maximize=False, title=None, white_bg=False): + """Constructor. + + :param bool maximize: Start process in new console window, maximized. + :param bool white_bg: New console window will be black text on white background. + :param bytes title: Set new window title to this instead of exe path. + """ + super(StartupInfo, self).__init__() + self.cb = ctypes.sizeof(self) + if maximize: + self.dwFlags |= STARTF_USESHOWWINDOW + self.wShowWindow = SW_MAXIMIZE + if title: + self.lpTitle = ctypes.c_char_p(title) + if white_bg: + self.dwFlags |= STARTF_USEFILLATTRIBUTE + self.dwFillAttribute = WINDOWS_CODES['hibgwhite'] | WINDOWS_CODES['black'] + + +class ProcessInfo(ctypes.Structure): + """PROCESS_INFORMATION structure.""" + + _fields_ = [ + ('hProcess', ctypes.c_void_p), + ('hThread', ctypes.c_void_p), + ('dwProcessId', ctypes.c_ulong), + ('dwThreadId', ctypes.c_ulong), + ] + + +class RunNewConsole(object): + """Run the command in a new console window. Windows only. Use in a with statement. + + subprocess sucks and really limits your access to the win32 API. Its implementation is half-assed. Using this so + that STARTUPINFO.lpTitle actually works and STARTUPINFO.dwFillAttribute produce the expected result. + """ + + def __init__(self, command, maximized=False, title=None, white_bg=False): + """Constructor. + + :param iter command: Command to run. + :param bool maximized: Start process in new console window, maximized. + :param bytes title: Set new window title to this. Needed by user32.FindWindow. + :param bool white_bg: New console window will be black text on white background. + """ + if title is None: + title = 'pytest-{0}-{1}'.format(os.getpid(), random.randint(1000, 9999)).encode('ascii') + self.startup_info = StartupInfo(maximize=maximized, title=title, white_bg=white_bg) + self.process_info = ProcessInfo() + self.command_str = subprocess.list2cmdline(command).encode('ascii') + self._handles = list() + self._kernel32 = ctypes.LibraryLoader(ctypes.WinDLL).kernel32 + self._kernel32.GetExitCodeProcess.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_ulong)] + self._kernel32.GetExitCodeProcess.restype = ctypes.c_long + + def __del__(self): + """Close win32 handles.""" + while self._handles: + try: + self._kernel32.CloseHandle(self._handles.pop(0)) # .pop() is thread safe. + except IndexError: + break + + def __enter__(self): + """Entering the `with` block. Runs the process.""" + if not self._kernel32.CreateProcessA( + None, # lpApplicationName + self.command_str, # lpCommandLine + None, # lpProcessAttributes + None, # lpThreadAttributes + False, # bInheritHandles + subprocess.CREATE_NEW_CONSOLE, # dwCreationFlags + None, # lpEnvironment + str(PROJECT_ROOT).encode('ascii'), # lpCurrentDirectory + ctypes.byref(self.startup_info), # lpStartupInfo + ctypes.byref(self.process_info) # lpProcessInformation + ): + raise ctypes.WinError() + + # Add handles added by the OS. + self._handles.append(self.process_info.hProcess) + self._handles.append(self.process_info.hThread) + + # Get hWnd. + self.hwnd = 0 + for _ in range(int(5 / 0.1)): + # Takes time for console window to initialize. + self.hwnd = ctypes.windll.user32.FindWindowA(None, self.startup_info.lpTitle) + if self.hwnd: + break + time.sleep(0.1) + assert self.hwnd + + # Return generator that yields window size/position. + return self._iter_pos() + + def __exit__(self, *_): + """Cleanup.""" + try: + # Verify process exited 0. + status = ctypes.c_ulong(STILL_ACTIVE) + while status.value == STILL_ACTIVE: + time.sleep(0.1) + if not self._kernel32.GetExitCodeProcess(self.process_info.hProcess, ctypes.byref(status)): + raise ctypes.WinError() + assert status.value == 0 + finally: + # Close handles. + self.__del__() + + def _iter_pos(self): + """Yield new console window's current position and dimensions. + + :return: Yields region the new window is in (left, upper, right, lower). + :rtype: tuple + """ + rect = ctypes.create_string_buffer(16) # To be written to by GetWindowRect. RECT structure. + while ctypes.windll.user32.GetWindowRect(self.hwnd, rect): + left, top, right, bottom = struct.unpack('llll', rect.raw) + width, height = right - left, bottom - top + assert width > 1 + assert height > 1 + yield left, top, right, bottom + raise StopIteration + + +def iter_rows(pil_image): + """Yield tuple of pixels for each row in the image. + + itertools.izip in Python 2.x and zip in Python 3.x are writen in C. Much faster than anything else I've found + written in pure Python. + + From: + http://stackoverflow.com/questions/1624883/alternative-way-to-split-a-list-into-groups-of-n/1625023#1625023 + + :param PIL.Image.Image pil_image: Image to read from. + + :return: Yields rows. + :rtype: tuple + """ + iterator = izip(*(iter(pil_image.getdata()),) * pil_image.width) + for row in iterator: + yield row + + +def get_most_interesting_row(pil_image): + """Look for a row in the image that has the most unique pixels. + + :param PIL.Image.Image pil_image: Image to read from. + + :return: Row (tuple of pixel tuples), row as a set, first pixel tuple, y offset from top. + :rtype: tuple + """ + final = (None, set(), None, None) # row, row_set, first_pixel, y_pos + for y_pos, row in enumerate(iter_rows(pil_image)): + row_set = set(row) + if len(row_set) > len(final[1]): + final = row, row_set, row[0], y_pos + if len(row_set) == pil_image.width: + break # Can't get bigger. + return final + + +def count_subimages(screenshot, subimg): + """Check how often subimg appears in the screenshot image. + + :param PIL.Image.Image screenshot: Screen shot to search through. + :param PIL.Image.Image subimg: Subimage to search for. + + :return: Number of times subimg appears in the screenshot. + :rtype: int + """ + # Get row to search for. + si_pixels = list(subimg.getdata()) # Load entire subimg into memory. + si_width = subimg.width + si_height = subimg.height + si_row, si_row_set, si_pixel, si_y = get_most_interesting_row(subimg) + occurrences = 0 + + # Look for subimg row in screenshot, then crop and compare pixel arrays. + for y_pos, row in enumerate(iter_rows(screenshot)): + if si_row_set - set(row): + continue # Some pixels not found. + for x_pos in range(screenshot.width - si_width + 1): + if row[x_pos] != si_pixel: + continue # First pixel does not match. + if row[x_pos:x_pos + si_width] != si_row: + continue # Row does not match. + # Found match for interesting row of subimg in screenshot. + y_corrected = y_pos - si_y + with screenshot.crop((x_pos, y_corrected, x_pos + si_width, y_corrected + si_height)) as cropped: + if list(cropped.getdata()) == si_pixels: + occurrences += 1 + + return occurrences + + +def try_candidates(screenshot, subimg_candidates, expected_count): + """Call count_subimages() for each subimage candidate until. + + If you get ImportError run "pip install pillow". Only OSX and Windows is supported. + + :param PIL.Image.Image screenshot: Screen shot to search through. + :param iter subimg_candidates: Subimage paths to look for. List of strings. + :param int expected_count: Try until any a subimage candidate is found this many times. + + :return: Number of times subimg appears in the screenshot. + :rtype: int + """ + from PIL import Image + count_found = 0 + + for subimg_path in subimg_candidates: + with Image.open(subimg_path) as rgba_s: + with rgba_s.convert(mode='RGB') as subimg: + # Make sure subimage isn't too large. + assert subimg.width < 256 + assert subimg.height < 256 + + # Count. + count_found = count_subimages(screenshot, subimg) + if count_found == expected_count: + break # No need to try other candidates. + + return count_found + + +def screenshot_until_match(save_to, timeout, subimg_candidates, expected_count, gen): + """Take screenshots until one of the 'done' subimages is found. Image is saved when subimage found or at timeout. + + If you get ImportError run "pip install pillow". Only OSX and Windows is supported. + + :param str save_to: Save screenshot to this PNG file path when expected count found or timeout. + :param int timeout: Give up after these many seconds. + :param iter subimg_candidates: Subimage paths to look for. List of strings. + :param int expected_count: Keep trying until any of subimg_candidates is found this many times. + :param iter gen: Generator yielding window position and size to crop screenshot to. + """ + from PIL import ImageGrab + assert save_to.endswith('.png') + stop_after = time.time() + timeout + + # Take screenshots until subimage is found. + while True: + with ImageGrab.grab(next(gen)) as rgba: + with rgba.convert(mode='RGB') as screenshot: + count_found = try_candidates(screenshot, subimg_candidates, expected_count) + if count_found == expected_count or time.time() > stop_after: + screenshot.save(save_to) + assert count_found == expected_count + return + time.sleep(0.5) diff --git a/tests/sub_box_green_win10.bmp b/tests/sub_box_green_win10.bmp Binary files differnew file mode 100644 index 0000000..485788e --- /dev/null +++ b/tests/sub_box_green_win10.bmp diff --git a/tests/sub_box_green_winxp.bmp b/tests/sub_box_green_winxp.bmp Binary files differnew file mode 100644 index 0000000..0823dd0 --- /dev/null +++ b/tests/sub_box_green_winxp.bmp diff --git a/tests/sub_box_sans_win10.bmp b/tests/sub_box_sans_win10.bmp Binary files differnew file mode 100644 index 0000000..48344b9 --- /dev/null +++ b/tests/sub_box_sans_win10.bmp diff --git a/tests/sub_box_sans_winxp.bmp b/tests/sub_box_sans_winxp.bmp Binary files differnew file mode 100644 index 0000000..8277d1e --- /dev/null +++ b/tests/sub_box_sans_winxp.bmp diff --git a/tests/sub_red_dark_fg_win10.bmp b/tests/sub_red_dark_fg_win10.bmp Binary files differnew file mode 100644 index 0000000..4d66fa9 --- /dev/null +++ b/tests/sub_red_dark_fg_win10.bmp diff --git a/tests/sub_red_dark_fg_winxp.bmp b/tests/sub_red_dark_fg_winxp.bmp Binary files differnew file mode 100644 index 0000000..2846c5a --- /dev/null +++ b/tests/sub_red_dark_fg_winxp.bmp diff --git a/tests/sub_red_light_fg_win10.bmp b/tests/sub_red_light_fg_win10.bmp Binary files differnew file mode 100644 index 0000000..43229a3 --- /dev/null +++ b/tests/sub_red_light_fg_win10.bmp diff --git a/tests/sub_red_light_fg_winxp.bmp b/tests/sub_red_light_fg_winxp.bmp Binary files differnew file mode 100644 index 0000000..e930459 --- /dev/null +++ b/tests/sub_red_light_fg_winxp.bmp diff --git a/tests/sub_red_sans_win10.bmp b/tests/sub_red_sans_win10.bmp Binary files differnew file mode 100644 index 0000000..738c172 --- /dev/null +++ b/tests/sub_red_sans_win10.bmp diff --git a/tests/sub_red_sans_winxp.bmp b/tests/sub_red_sans_winxp.bmp Binary files differnew file mode 100644 index 0000000..a9f0f2e --- /dev/null +++ b/tests/sub_red_sans_winxp.bmp diff --git a/tests/test___main__.py b/tests/test___main__.py new file mode 100644 index 0000000..17a4b76 --- /dev/null +++ b/tests/test___main__.py @@ -0,0 +1,64 @@ +"""Test objects in module.""" + +import subprocess +import sys + +import pytest + +from colorclass.windows import IS_WINDOWS + + +def test_import_do_nothing(): + """Make sure importing __main__ doesn't print anything.""" + command = [sys.executable, '-c', "from colorclass.__main__ import TRUTHY; assert TRUTHY"] + proc = subprocess.Popen(command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) + output = proc.communicate() + assert proc.poll() == 0 + assert not output[0] + assert not output[1] + + +@pytest.mark.parametrize('colors', [True, False, None]) +@pytest.mark.parametrize('light', [True, False, None]) +def test(monkeypatch, colors, light): + """Test package as a script. + + :param monkeypatch: pytest fixture. + :param bool colors: Enable, disable, or don't touch colors using CLI args or env variables. + :param bool light: Enable light, dark, or don't touch auto colors using CLI args or env variables. + """ + command = [sys.executable, '-m', 'colorclass' if sys.version_info >= (2, 7) else 'colorclass.__main__'] + stdin = '{autored}Red{/autored} {red}Red{/red} {hired}Red{/hired}'.encode() + + # Set options. + if colors is True: + monkeypatch.setenv('COLOR_ENABLE', 'true') + elif colors is False: + monkeypatch.setenv('COLOR_DISABLE', 'true') + if light is True: + monkeypatch.setenv('COLOR_LIGHT', 'true') + elif light is False: + monkeypatch.setenv('COLOR_DARK', 'true') + + # Run. + proc = subprocess.Popen(command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, stdin=subprocess.PIPE) + output = proc.communicate(stdin)[0].decode() + assert proc.poll() == 0 + assert 'Red' in output + + # Verify colors. Output is always stripped of all colors on Windows when piped to non-console (e.g. pytest). + if colors is False or IS_WINDOWS: + assert '\033[' not in output + assert 'Red Red Red' in output + return + assert '\033[' in output + + # Verify light bg. + count_dark_fg = output.count('\033[31mRed') + count_light_fg = output.count('\033[91mRed') + if light: + assert count_dark_fg == 2 + assert count_light_fg == 1 + else: + assert count_dark_fg == 1 + assert count_light_fg == 2 diff --git a/tests/test_codes.py b/tests/test_codes.py new file mode 100644 index 0000000..dd146c8 --- /dev/null +++ b/tests/test_codes.py @@ -0,0 +1,137 @@ +"""Test objects in module.""" + +import errno +import os +import subprocess +import sys +import time + +import pytest + +from colorclass.codes import ANSICodeMapping, BASE_CODES, list_tags +from colorclass.windows import IS_WINDOWS + + +def test_ansi_code_mapping_whitelist(): + """Test whitelist enforcement.""" + auto_codes = ANSICodeMapping('{green}{bgred}Test{/all}') + + # Test __getitem__. + with pytest.raises(KeyError): + assert not auto_codes['red'] + assert auto_codes['green'] == 32 + + # Test iter and len. + assert sorted(auto_codes) == ['/all', 'bgred', 'green'] + assert len(auto_codes) == 3 + + +@pytest.mark.parametrize('toggle', ['light', 'dark', 'none']) +def test_auto_toggles(toggle): + """Test auto colors and ANSICodeMapping class toggles. + + :param str toggle: Toggle method to call. + """ + # Toggle. + if toggle == 'light': + ANSICodeMapping.enable_all_colors() + ANSICodeMapping.set_light_background() + assert ANSICodeMapping.DISABLE_COLORS is False + assert ANSICodeMapping.LIGHT_BACKGROUND is True + elif toggle == 'dark': + ANSICodeMapping.enable_all_colors() + ANSICodeMapping.set_dark_background() + assert ANSICodeMapping.DISABLE_COLORS is False + assert ANSICodeMapping.LIGHT_BACKGROUND is False + else: + ANSICodeMapping.disable_all_colors() + assert ANSICodeMapping.DISABLE_COLORS is True + assert ANSICodeMapping.LIGHT_BACKGROUND is False + + # Test iter and len. + auto_codes = ANSICodeMapping('}{'.join([''] + list(BASE_CODES) + [''])) + count = 0 + for k, v in auto_codes.items(): + count += 1 + assert str(k) == k + assert v is None or int(v) == v + assert len(auto_codes) == count + + # Test foreground properties. + key_fg = '{autoblack}{autored}{autogreen}{autoyellow}{autoblue}{automagenta}{autocyan}{autowhite}' + actual = key_fg.format(**auto_codes) + if toggle == 'light': + assert actual == '3031323334353637' + elif toggle == 'dark': + assert actual == '9091929394959697' + else: + assert actual == 'NoneNoneNoneNoneNoneNoneNoneNone' + + # Test background properties. + key_fg = '{autobgblack}{autobgred}{autobggreen}{autobgyellow}{autobgblue}{autobgmagenta}{autobgcyan}{autobgwhite}' + actual = key_fg.format(**auto_codes) + if toggle == 'light': + assert actual == '4041424344454647' + elif toggle == 'dark': + assert actual == '100101102103104105106107' + else: + assert actual == 'NoneNoneNoneNoneNoneNoneNoneNone' + + +def test_list_tags(): + """Test list_tags().""" + actual = list_tags() + assert ('red', '/red', 31, 39) in actual + assert sorted(t for i in actual for t in i[:2] if t is not None) == sorted(BASE_CODES) + + +@pytest.mark.parametrize('tty', [False, True]) +def test_disable_colors_piped(tty): + """Verify colors enabled by default when piped to TTY and disabled when not. + + :param bool tty: Pipe to TTY/terminal? + """ + assert_statement = 'assert __import__("colorclass").codes.ANSICodeMapping.disable_if_no_tty() is {bool}' + command_colors_enabled = [sys.executable, '-c', assert_statement.format(bool='False')] + command_colors_disabled = [sys.executable, '-c', assert_statement.format(bool='True')] + + # Run piped to this pytest process. + if not tty: # Outputs piped to non-terminal/non-tty. Colors disabled by default. + proc = subprocess.Popen(command_colors_disabled, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) + output = proc.communicate() + assert not output[0] + assert not output[1] + assert proc.poll() == 0 + return + + # Run through a new console window (Windows). + if IS_WINDOWS: + c_flags = subprocess.CREATE_NEW_CONSOLE + proc = subprocess.Popen(command_colors_enabled, close_fds=True, creationflags=c_flags) + proc.communicate() # Pipes directed towards new console window. Not worth doing screenshot image processing. + assert proc.poll() == 0 + return + + # Run through pseudo tty (Linux/OSX). + master, slave = __import__('pty').openpty() + proc = subprocess.Popen(command_colors_enabled, stderr=subprocess.STDOUT, stdout=slave, close_fds=True) + os.close(slave) + + # Read output. + output = '' + while True: + try: + data = os.read(master, 1024).decode() + except OSError as exc: + if exc.errno != errno.EIO: # EIO means EOF on some systems. + raise + data = None + if data: + output += data + elif proc.poll() is None: + time.sleep(0.01) + else: + break + os.close(master) + assert not output + assert proc.poll() == 0 diff --git a/tests/test_color.py b/tests/test_color.py new file mode 100644 index 0000000..d5c7170 --- /dev/null +++ b/tests/test_color.py @@ -0,0 +1,185 @@ +"""Test objects in module.""" + +import sys +from functools import partial + +import pytest + +from colorclass.color import Color +from tests.conftest import assert_both_values, get_instance + + +def test_colorize_methods(): + """Test colorize convenience methods.""" + assert Color.black('TEST').value_colors == '\033[30mTEST\033[39m' + assert Color.bgblack('TEST').value_colors == '\033[40mTEST\033[49m' + assert Color.red('TEST').value_colors == '\033[31mTEST\033[39m' + assert Color.bgred('TEST').value_colors == '\033[41mTEST\033[49m' + assert Color.green('TEST').value_colors == '\033[32mTEST\033[39m' + assert Color.bggreen('TEST').value_colors == '\033[42mTEST\033[49m' + assert Color.yellow('TEST').value_colors == '\033[33mTEST\033[39m' + assert Color.bgyellow('TEST').value_colors == '\033[43mTEST\033[49m' + assert Color.blue('TEST').value_colors == '\033[34mTEST\033[39m' + assert Color.bgblue('TEST').value_colors == '\033[44mTEST\033[49m' + assert Color.magenta('TEST').value_colors == '\033[35mTEST\033[39m' + assert Color.bgmagenta('TEST').value_colors == '\033[45mTEST\033[49m' + assert Color.cyan('TEST').value_colors == '\033[36mTEST\033[39m' + assert Color.bgcyan('TEST').value_colors == '\033[46mTEST\033[49m' + assert Color.white('TEST').value_colors == '\033[37mTEST\033[39m' + assert Color.bgwhite('TEST').value_colors == '\033[47mTEST\033[49m' + + assert Color.black('this is a test.', auto=True) == Color('{autoblack}this is a test.{/autoblack}') + assert Color.black('this is a test.') == Color('{black}this is a test.{/black}') + assert Color.bgblack('this is a test.', auto=True) == Color('{autobgblack}this is a test.{/autobgblack}') + assert Color.bgblack('this is a test.') == Color('{bgblack}this is a test.{/bgblack}') + assert Color.red('this is a test.', auto=True) == Color('{autored}this is a test.{/autored}') + assert Color.red('this is a test.') == Color('{red}this is a test.{/red}') + assert Color.bgred('this is a test.', auto=True) == Color('{autobgred}this is a test.{/autobgred}') + assert Color.bgred('this is a test.') == Color('{bgred}this is a test.{/bgred}') + assert Color.green('this is a test.', auto=True) == Color('{autogreen}this is a test.{/autogreen}') + assert Color.green('this is a test.') == Color('{green}this is a test.{/green}') + assert Color.bggreen('this is a test.', auto=True) == Color('{autobggreen}this is a test.{/autobggreen}') + assert Color.bggreen('this is a test.') == Color('{bggreen}this is a test.{/bggreen}') + assert Color.yellow('this is a test.', auto=True) == Color('{autoyellow}this is a test.{/autoyellow}') + assert Color.yellow('this is a test.') == Color('{yellow}this is a test.{/yellow}') + assert Color.bgyellow('this is a test.', auto=True) == Color('{autobgyellow}this is a test.{/autobgyellow}') + assert Color.bgyellow('this is a test.') == Color('{bgyellow}this is a test.{/bgyellow}') + assert Color.blue('this is a test.', auto=True) == Color('{autoblue}this is a test.{/autoblue}') + assert Color.blue('this is a test.') == Color('{blue}this is a test.{/blue}') + assert Color.bgblue('this is a test.', auto=True) == Color('{autobgblue}this is a test.{/autobgblue}') + assert Color.bgblue('this is a test.') == Color('{bgblue}this is a test.{/bgblue}') + assert Color.magenta('this is a test.', auto=True) == Color('{automagenta}this is a test.{/automagenta}') + assert Color.magenta('this is a test.') == Color('{magenta}this is a test.{/magenta}') + assert Color.bgmagenta('this is a test.', auto=True) == Color('{autobgmagenta}this is a test.{/autobgmagenta}') + assert Color.bgmagenta('this is a test.') == Color('{bgmagenta}this is a test.{/bgmagenta}') + assert Color.cyan('this is a test.', auto=True) == Color('{autocyan}this is a test.{/autocyan}') + assert Color.cyan('this is a test.') == Color('{cyan}this is a test.{/cyan}') + assert Color.bgcyan('this is a test.', auto=True) == Color('{autobgcyan}this is a test.{/autobgcyan}') + assert Color.bgcyan('this is a test.') == Color('{bgcyan}this is a test.{/bgcyan}') + assert Color.white('this is a test.', auto=True) == Color('{autowhite}this is a test.{/autowhite}') + assert Color.white('this is a test.') == Color('{white}this is a test.{/white}') + assert Color.bgwhite('this is a test.', auto=True) == Color('{autobgwhite}this is a test.{/autobgwhite}') + assert Color.bgwhite('this is a test.') == Color('{bgwhite}this is a test.{/bgwhite}') + + +@pytest.mark.parametrize('kind', ['str', 'Color plain', 'Color color']) +def test_chaining(kind): + """Test chaining Color instances. + + :param str kind: Type of string to test. + """ + assert_both = partial(assert_both_values, kind=kind) + + # Test string. + instance = get_instance(kind, 'TEST') + for color in ('green', 'blue', 'yellow'): + instance = get_instance(kind, instance, color) + assert_both(instance, 'TEST', '\033[31mTEST\033[39m') + + # Test empty. + instance = get_instance(kind) + for color in ('red', 'green', 'blue', 'yellow'): + instance = get_instance(kind, instance, color) + assert_both(instance, '', '\033[39m') + + # Test complicated. + instance = 'TEST' + for color in ('black', 'bgred', 'green', 'bgyellow', 'blue', 'bgmagenta', 'cyan', 'bgwhite'): + instance = get_instance(kind, instance, color=color) + assert_both(instance, 'TEST', '\033[30;41mTEST\033[39;49m') + + # Test format and length. + instance = get_instance(kind, '{0}').format(get_instance(kind, 'TEST')) + assert_both(instance, 'TEST', '\033[31mTEST\033[39m') + assert len(instance) == 4 + instance = get_instance(kind, '{0}').format(instance) + assert_both(instance, 'TEST', '\033[31mTEST\033[39m') + assert len(instance) == 4 + instance = get_instance(kind, '{0}').format(instance) + assert_both(instance, 'TEST', '\033[31mTEST\033[39m') + assert len(instance) == 4 + + +@pytest.mark.parametrize('kind', ['str', 'Color plain', 'Color color']) +def test_empty(kind): + """Test with empty string. + + :param str kind: Type of string to test. + """ + instance = get_instance(kind, u'') + assert_both = partial(assert_both_values, kind=kind) + + assert len(instance) == 0 + assert_both(instance * 2, '', '\033[39m') + assert_both(instance + instance, '', '\033[39m') + with pytest.raises(IndexError): + assert instance[0] + assert not [i for i in instance] + assert not list(instance) + + assert instance.encode('utf-8') == instance.encode('utf-8') + assert instance.encode('utf-8').decode('utf-8') == instance + assert_both(instance.encode('utf-8').decode('utf-8'), '', '\033[39m') + assert_both(instance.__class__.encode(instance, 'utf-8').decode('utf-8'), '', '\033[39m') + assert len(instance.encode('utf-8').decode('utf-8')) == 0 + assert_both(instance.format(value=''), '', '\033[39m') + + assert_both(instance.capitalize(), '', '\033[39m') + assert_both(instance.center(5), ' ', '\033[39m ') + assert instance.count('') == 1 + assert instance.count('t') == 0 + assert instance.endswith('') is True + assert instance.endswith('me') is False + assert instance.find('') == 0 + assert instance.find('t') == -1 + + assert instance.index('') == 0 + with pytest.raises(ValueError): + assert instance.index('t') + assert instance.isalnum() is False + assert instance.isalpha() is False + if sys.version_info[0] != 2: + assert instance.isdecimal() is False + assert instance.isdigit() is False + if sys.version_info[0] != 2: + assert instance.isnumeric() is False + assert instance.isspace() is False + assert instance.istitle() is False + assert instance.isupper() is False + + assert_both(instance.join(['A', 'B']), 'AB', 'A\033[39mB') + assert_both(instance.ljust(5), ' ', '\033[39m ') + assert instance.rfind('') == 0 + assert instance.rfind('t') == -1 + assert instance.rindex('') == 0 + with pytest.raises(ValueError): + assert instance.rindex('t') + assert_both(instance.rjust(5), ' ', '\033[39m ') + if kind in ('str', 'Color plain'): + assert instance.splitlines() == list() + else: + assert instance.splitlines() == ['\033[39m'] + assert instance.startswith('') is True + assert instance.startswith('T') is False + assert_both(instance.swapcase(), '', '\033[39m') + + assert_both(instance.title(), '', '\033[39m') + assert_both(instance.translate({ord('t'): u'1', ord('e'): u'2', ord('s'): u'3'}), '', '\033[39m') + assert_both(instance.upper(), '', '\033[39m') + assert_both(instance.zfill(0), '', '') + assert_both(instance.zfill(1), '0', '0') + + +def test_keep_tags(): + """Test keep_tags keyword arg.""" + assert_both = partial(assert_both_values, kind='Color color') + + instance = Color('{red}Test{/red}', keep_tags=True) + assert_both(instance, '{red}Test{/red}', '{red}Test{/red}') + assert_both(instance.upper(), '{RED}TEST{/RED}', '{RED}TEST{/RED}') + assert len(instance) == 15 + + instance = Color('{red}\033[41mTest\033[49m{/red}', keep_tags=True) + assert_both(instance, '{red}Test{/red}', '{red}\033[41mTest\033[49m{/red}') + assert_both(instance.upper(), '{RED}TEST{/RED}', '{RED}\033[41mTEST\033[49m{/RED}') + assert len(instance) == 15 diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..2d398cd --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,398 @@ +"""Test objects in module.""" + +import sys +from functools import partial + +import pytest + +from colorclass.core import apply_text, ColorStr +from tests.conftest import assert_both_values, get_instance + + +def test_apply_text(): + """Test apply_text().""" + assert apply_text('', lambda _: 0 / 0) == '' + assert apply_text('TEST', lambda s: s.lower()) == 'test' + assert apply_text('!\033[31mRed\033[0m', lambda s: s.upper()) == '!\033[31mRED\033[0m' + assert apply_text('\033[1mA \033[31mB \033[32;41mC \033[0mD', lambda _: '') == '\033[1m\033[31m\033[32;41m\033[0m' + + +@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color']) +def test_dunder(kind): + """Test "dunder" methods (double-underscore). + + :param str kind: Type of string to test. + """ + instance = get_instance(kind, 'test ME ') + assert_both = partial(assert_both_values, kind=kind) + + assert len(instance) == 8 + + if kind == 'str': + assert repr(instance) == "'test ME '" + elif kind == 'ColorStr plain': + assert repr(instance) == "ColorStr('test ME ')" + else: + assert repr(instance) == "ColorStr('\\x1b[31mtest ME \\x1b[39m')" + + assert_both(instance.__class__('1%s2' % instance), '1test ME 2', '1\033[31mtest ME \033[39m2') + assert_both(get_instance(kind, '1%s2') % 'test ME ', '1test ME 2', '\033[31m1test ME 2\033[39m') + assert_both(get_instance(kind, '1%s2') % instance, '1test ME 2', '\033[31m1test ME \033[39m2') + + assert_both(instance * 2, 'test ME test ME ', '\033[31mtest ME test ME \033[39m') + assert_both(instance + instance, 'test ME test ME ', '\033[31mtest ME test ME \033[39m') + assert_both(instance + 'more', 'test ME more', '\033[31mtest ME \033[39mmore') + assert_both(instance.__class__('more' + instance), 'moretest ME ', 'more\033[31mtest ME \033[39m') + instance *= 2 + assert_both(instance, 'test ME test ME ', '\033[31mtest ME test ME \033[39m') + instance += 'more' + assert_both(instance, 'test ME test ME more', '\033[31mtest ME test ME \033[39mmore') + + assert_both(instance[0], 't', '\033[31mt\033[39m') + assert_both(instance[4], ' ', '\033[31m \033[39m') + assert_both(instance[-1], 'e', '\033[39me') + # assert_both(instance[1:-1], 'est ME test ME mor', '\033[31mest ME test ME \033[39mmor') + # assert_both(instance[1:9:2], 'etM ', '\033[31metM \033[39m') + # assert_both(instance[-1::-1], 'erom EM tset EM tset', 'erom\033[31m EM tset EM tset\033[39m') + + with pytest.raises(IndexError): + assert instance[20] + + actual = [i for i in instance] + assert len(actual) == 20 + assert actual == list(instance) + assert_both(actual[0], 't', '\033[31mt\033[39m') + assert_both(actual[1], 'e', '\033[31me\033[39m') + assert_both(actual[2], 's', '\033[31ms\033[39m') + assert_both(actual[3], 't', '\033[31mt\033[39m') + assert_both(actual[4], ' ', '\033[31m \033[39m') + assert_both(actual[5], 'M', '\033[31mM\033[39m') + assert_both(actual[6], 'E', '\033[31mE\033[39m') + assert_both(actual[7], ' ', '\033[31m \033[39m') + assert_both(actual[8], 't', '\033[31mt\033[39m') + assert_both(actual[9], 'e', '\033[31me\033[39m') + assert_both(actual[10], 's', '\033[31ms\033[39m') + assert_both(actual[11], 't', '\033[31mt\033[39m') + assert_both(actual[12], ' ', '\033[31m \033[39m') + assert_both(actual[13], 'M', '\033[31mM\033[39m') + assert_both(actual[14], 'E', '\033[31mE\033[39m') + assert_both(actual[15], ' ', '\033[31m \033[39m') + assert_both(actual[16], 'm', '\033[39mm') + assert_both(actual[17], 'o', '\033[39mo') + assert_both(actual[18], 'r', '\033[39mr') + assert_both(actual[19], 'e', '\033[39me') + + +@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color']) +def test_encode_decode(kind): + """Test encode and decode methods. + + :param str kind: Type of string to test. + """ + assert_both = partial(assert_both_values, kind=kind) + instance = get_instance(kind, 'test ME') + + if sys.version_info[0] == 2: + assert instance.encode('utf-8') == instance + assert instance.decode('utf-8') == instance + assert_both(instance.decode('utf-8'), 'test ME', '\033[31mtest ME\033[39m') + assert_both(instance.__class__.decode(instance, 'utf-8'), 'test ME', '\033[31mtest ME\033[39m') + assert len(instance.decode('utf-8')) == 7 + else: + assert instance.encode('utf-8') != instance + assert instance.encode('utf-8') == instance.encode('utf-8') + assert instance.encode('utf-8').decode('utf-8') == instance + assert_both(instance.encode('utf-8').decode('utf-8'), 'test ME', '\033[31mtest ME\033[39m') + assert_both(instance.__class__.encode(instance, 'utf-8').decode('utf-8'), 'test ME', '\033[31mtest ME\033[39m') + assert len(instance.encode('utf-8').decode('utf-8')) == 7 + + +@pytest.mark.parametrize('mode', ['fg within bg', 'bg within fg']) +@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color']) +def test_format(kind, mode): + """Test format method. + + :param str kind: Type of string to test. + :param str mode: Which combination to test. + """ + assert_both = partial(assert_both_values, kind=kind) + + # Test str.format(ColorStr()). + instance = get_instance(kind, 'test me') + assert_both(instance.__class__('1{0}2'.format(instance)), '1test me2', '1\033[31mtest me\033[39m2') + assert_both(instance.__class__(str.format('1{0}2', instance)), '1test me2', '1\033[31mtest me\033[39m2') + + # Get actual. + template_pos = get_instance(kind, 'a{0}c{0}', 'bgred' if mode == 'fg within bg' else 'red') + template_kw = get_instance(kind, 'a{value}c{value}', 'bgred' if mode == 'fg within bg' else 'red') + instance = get_instance(kind, 'B', 'green' if mode == 'fg within bg' else 'bggreen') + + # Get expected. + expected = ['aBcB', None] + if mode == 'fg within bg': + expected[1] = '\033[41ma\033[32mB\033[39mc\033[32mB\033[39;49m' + else: + expected[1] = '\033[31ma\033[42mB\033[49mc\033[42mB\033[39;49m' + + # Test. + assert_both(template_pos.format(instance), expected[0], expected[1]) + assert_both(template_kw.format(value=instance), expected[0], expected[1]) + assert_both(instance.__class__.format(template_pos, instance), expected[0], expected[1]) + assert_both(instance.__class__.format(template_kw, value=instance), expected[0], expected[1]) + + +@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color']) +def test_format_mixed(kind): + """Test format method with https://github.com/Robpol86/colorclass/issues/16 in mind. + + :param str kind: Type of string to test. + """ + instance = get_instance(kind, 'XXX: ') + '{0}' + assert_both = partial(assert_both_values, kind=kind) + + assert_both(instance, 'XXX: {0}', '\033[31mXXX: \033[39m{0}') + assert_both(instance.format('{blue}Moo{/blue}'), 'XXX: {blue}Moo{/blue}', '\033[31mXXX: \033[39m{blue}Moo{/blue}') + assert_both(instance.format(get_instance(kind, 'Moo', 'blue')), 'XXX: Moo', '\033[31mXXX: \033[34mMoo\033[39m') + + +@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color']) +def test_c_f(kind): + """Test C through F methods. + + :param str kind: Type of string to test. + """ + instance = get_instance(kind, 'test me') + assert_both = partial(assert_both_values, kind=kind) + + assert_both(instance.capitalize(), 'Test me', '\033[31mTest me\033[39m') + + assert_both(instance.center(11), ' test me ', ' \033[31mtest me\033[39m ') + assert_both(instance.center(11, '.'), '..test me..', '..\033[31mtest me\033[39m..') + assert_both(instance.center(12), ' test me ', ' \033[31mtest me\033[39m ') + + assert instance.count('t') == 2 + + assert instance.endswith('me') is True + assert instance.endswith('ME') is False + + assert instance.find('t') == 0 + assert instance.find('t', 0) == 0 + assert instance.find('t', 0, 1) == 0 + assert instance.find('t', 1) == 3 + assert instance.find('t', 1, 4) == 3 + assert instance.find('t', 1, 3) == -1 + assert instance.find('x') == -1 + assert instance.find('m') == 5 + + +@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color']) +def test_i(kind): + """Test I methods. + + :param str kind: Type of string to test. + """ + instance = get_instance(kind, 'tantamount') + assert instance.index('t') == 0 + assert instance.index('t', 0) == 0 + assert instance.index('t', 0, 1) == 0 + assert instance.index('t', 1) == 3 + assert instance.index('t', 1, 4) == 3 + assert instance.index('m') == 5 + with pytest.raises(ValueError): + assert instance.index('t', 1, 3) + with pytest.raises(ValueError): + assert instance.index('x') + + assert instance.isalnum() is True + assert get_instance(kind, '123').isalnum() is True + assert get_instance(kind, '.').isalnum() is False + + assert instance.isalpha() is True + assert get_instance(kind, '.').isalpha() is False + + if sys.version_info[0] != 2: + assert instance.isdecimal() is False + assert get_instance(kind, '123').isdecimal() is True + assert get_instance(kind, '.').isdecimal() is False + + assert instance.isdigit() is False + assert get_instance(kind, '123').isdigit() is True + assert get_instance(kind, '.').isdigit() is False + + if sys.version_info[0] != 2: + assert instance.isnumeric() is False + assert get_instance(kind, '123').isnumeric() is True + assert get_instance(kind, '.').isnumeric() is False + + assert instance.isspace() is False + assert get_instance(kind, ' ').isspace() is True + + assert instance.istitle() is False + assert get_instance(kind, 'Test').istitle() is True + + assert instance.isupper() is False + assert get_instance(kind, 'TEST').isupper() is True + + +@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color']) +def test_j_s(kind): + """Test J to S methods. + + :param str kind: Type of string to test. + """ + instance = get_instance(kind, 'test me') + assert_both = partial(assert_both_values, kind=kind) + + assert_both(instance.join(['A', 'B']), 'Atest meB', 'A\033[31mtest me\033[39mB') + iterable = [get_instance(kind, 'A', 'green'), get_instance(kind, 'B', 'green')] + assert_both(instance.join(iterable), 'Atest meB', '\033[32mA\033[31mtest me\033[32mB\033[39m') + + assert_both(instance.ljust(11), 'test me ', '\033[31mtest me\033[39m ') + assert_both(instance.ljust(11, '.'), 'test me....', '\033[31mtest me\033[39m....') + assert_both(instance.ljust(12), 'test me ', '\033[31mtest me\033[39m ') + + assert instance.rfind('t') == 3 + assert instance.rfind('t', 0) == 3 + assert instance.rfind('t', 0, 4) == 3 + assert instance.rfind('t', 0, 3) == 0 + assert instance.rfind('t', 3, 3) == -1 + assert instance.rfind('x') == -1 + assert instance.rfind('m') == 5 + + tantamount = get_instance(kind, 'tantamount') + assert tantamount.rindex('t') == 9 + assert tantamount.rindex('t', 0) == 9 + assert tantamount.rindex('t', 0, 5) == 3 + assert tantamount.rindex('m') == 5 + with pytest.raises(ValueError): + assert tantamount.rindex('t', 1, 3) + with pytest.raises(ValueError): + assert tantamount.rindex('x') + + assert_both(instance.rjust(11), ' test me', ' \033[31mtest me\033[39m') + assert_both(instance.rjust(11, '.'), '....test me', '....\033[31mtest me\033[39m') + assert_both(instance.rjust(12), ' test me', ' \033[31mtest me\033[39m') + + actual = get_instance(kind, '1\n2\n3').splitlines() + assert len(actual) == 3 + # assert_both(actual[0], '1', '\033[31m1\033[39m') + # assert_both(actual[1], '2', '\033[31m2\033[39m') + # assert_both(actual[2], '3', '\033[31m3\033[39m') + + assert instance.startswith('t') is True + assert instance.startswith('T') is False + + assert_both(get_instance(kind, 'AbC').swapcase(), 'aBc', '\033[31maBc\033[39m') + + +@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color']) +def test_t_z(kind): + """Test T to Z methods. + + :param str kind: Type of string to test. + """ + instance = get_instance(kind, u'test me') + assert_both = partial(assert_both_values, kind=kind) + + assert_both(instance.title(), 'Test Me', '\033[31mTest Me\033[39m') + assert_both(get_instance(kind, 'TEST YOU').title(), 'Test You', '\033[31mTest You\033[39m') + + table = {ord('t'): u'1', ord('e'): u'2', ord('s'): u'3'} + assert_both(instance.translate(table), '1231 m2', '\033[31m1231 m2\033[39m') + + assert_both(instance.upper(), 'TEST ME', '\033[31mTEST ME\033[39m') + + number = get_instance(kind, '350') + assert_both(number.zfill(1), '350', '\033[31m350\033[39m') + assert_both(number.zfill(2), '350', '\033[31m350\033[39m') + assert_both(number.zfill(3), '350', '\033[31m350\033[39m') + assert_both(number.zfill(4), '0350', '\033[31m0350\033[39m') + assert_both(number.zfill(10), '0000000350', '\033[31m0000000350\033[39m') + assert_both(get_instance(kind, '-350').zfill(5), '-0350', '\033[31m-0350\033[39m') + assert_both(get_instance(kind, '-10.3').zfill(5), '-10.3', '\033[31m-10.3\033[39m') + assert_both(get_instance(kind, '-10.3').zfill(6), '-010.3', '\033[31m-010.3\033[39m') + + +@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color']) +def test_empty(kind): + """Test with empty string. + + :param str kind: Type of string to test. + """ + instance = get_instance(kind, u'') + assert_both = partial(assert_both_values, kind=kind) + + assert len(instance) == 0 + assert_both(instance * 2, '', '\033[39m') + assert_both(instance + instance, '', '\033[39m') + with pytest.raises(IndexError): + assert instance[0] + assert not [i for i in instance] + assert not list(instance) + + assert instance.encode('utf-8') == instance.encode('utf-8') + assert instance.encode('utf-8').decode('utf-8') == instance + assert_both(instance.encode('utf-8').decode('utf-8'), '', '\033[39m') + assert_both(instance.__class__.encode(instance, 'utf-8').decode('utf-8'), '', '\033[39m') + assert len(instance.encode('utf-8').decode('utf-8')) == 0 + assert_both(instance.format(value=''), '', '\033[39m') + + assert_both(instance.capitalize(), '', '\033[39m') + assert_both(instance.center(5), ' ', '\033[39m ') + assert instance.count('') == 1 + assert instance.count('t') == 0 + assert instance.endswith('') is True + assert instance.endswith('me') is False + assert instance.find('') == 0 + assert instance.find('t') == -1 + + assert instance.index('') == 0 + with pytest.raises(ValueError): + assert instance.index('t') + assert instance.isalnum() is False + assert instance.isalpha() is False + if sys.version_info[0] != 2: + assert instance.isdecimal() is False + assert instance.isdigit() is False + if sys.version_info[0] != 2: + assert instance.isnumeric() is False + assert instance.isspace() is False + assert instance.istitle() is False + assert instance.isupper() is False + + assert_both(instance.join(['A', 'B']), 'AB', 'A\033[39mB') + assert_both(instance.ljust(5), ' ', '\033[39m ') + assert instance.rfind('') == 0 + assert instance.rfind('t') == -1 + assert instance.rindex('') == 0 + with pytest.raises(ValueError): + assert instance.rindex('t') + assert_both(instance.rjust(5), ' ', '\033[39m ') + if kind in ('str', 'ColorStr plain'): + assert instance.splitlines() == list() + else: + assert instance.splitlines() == ['\033[39m'] + assert instance.startswith('') is True + assert instance.startswith('T') is False + assert_both(instance.swapcase(), '', '\033[39m') + + assert_both(instance.title(), '', '\033[39m') + assert_both(instance.translate({ord('t'): u'1', ord('e'): u'2', ord('s'): u'3'}), '', '\033[39m') + assert_both(instance.upper(), '', '\033[39m') + assert_both(instance.zfill(0), '', '') + assert_both(instance.zfill(1), '0', '0') + + +def test_keep_tags(): + """Test keep_tags keyword arg.""" + assert_both = partial(assert_both_values, kind='ColorStr color') + + instance = ColorStr('{red}Test{/red}', keep_tags=True) + assert_both(instance, '{red}Test{/red}', '{red}Test{/red}') + assert_both(instance.upper(), '{RED}TEST{/RED}', '{RED}TEST{/RED}') + assert len(instance) == 15 + + instance = ColorStr('{red}\033[41mTest\033[49m{/red}', keep_tags=True) + assert_both(instance, '{red}Test{/red}', '{red}\033[41mTest\033[49m{/red}') + assert_both(instance.upper(), '{RED}TEST{/RED}', '{RED}\033[41mTEST\033[49m{/RED}') + assert len(instance) == 15 diff --git a/tests/test_example.py b/tests/test_example.py new file mode 100644 index 0000000..7ee8c05 --- /dev/null +++ b/tests/test_example.py @@ -0,0 +1,96 @@ +"""Test example script.""" + +import subprocess +import sys + +import pytest + +from colorclass.windows import IS_WINDOWS +from tests.conftest import PROJECT_ROOT +from tests.screenshot import RunNewConsole, screenshot_until_match + + +@pytest.mark.parametrize('colors', [True, False, None]) +@pytest.mark.parametrize('light_bg', [True, False, None]) +def test_piped(colors, light_bg): + """Test script with output piped to non-tty (this pytest process). + + :param bool colors: Enable, disable, or omit color arguments (default is no colors due to no tty). + :param bool light_bg: Enable light, dark, or omit light/dark arguments. + """ + command = [sys.executable, str(PROJECT_ROOT.join('example.py')), 'print'] + + # Set options. + if colors is True: + command.append('--colors') + elif colors is False: + command.append('--no-colors') + if light_bg is True: + command.append('--light-bg') + elif light_bg is False: + command.append('--dark-bg') + + # Run. + proc = subprocess.Popen(command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) + output = proc.communicate()[0].decode() + assert proc.poll() == 0 + assert 'Autocolors for all backgrounds' in output + assert 'Red' in output + + # Verify colors. Output is always stripped of all colors on Windows when piped to non-console (e.g. pytest). + if colors is False or IS_WINDOWS: + assert '\033[' not in output + assert 'Black Red Green Yellow Blue Magenta Cyan White' in output + return + assert '\033[' in output + + # Verify light bg. + count_dark_fg = output.count('\033[31mRed') + count_light_fg = output.count('\033[91mRed') + if light_bg: + assert count_dark_fg == 2 + assert count_light_fg == 1 + else: + assert count_dark_fg == 1 + assert count_light_fg == 2 + + +@pytest.mark.skipif(str(not IS_WINDOWS)) +@pytest.mark.parametrize('colors,light_bg', [ + (True, False), + (True, True), + (False, False), + (None, False), +]) +def test_windows_screenshot(colors, light_bg): + """Test script on Windows in a new console window. Take a screenshot to verify colors work. + + :param bool colors: Enable, disable, or omit color arguments (default has colors). + :param bool light_bg: Create console with white background color. + """ + screenshot = PROJECT_ROOT.join('test_example_test_windows_screenshot.png') + if screenshot.check(): + screenshot.remove() + command = [sys.executable, str(PROJECT_ROOT.join('example.py')), 'print', '-w', str(screenshot)] + + # Set options. + if colors is True: + command.append('--colors') + elif colors is False: + command.append('--no-colors') + + # Setup expected. + if colors is False: + candidates = [str(p) for p in PROJECT_ROOT.join('tests').listdir('sub_red_sans_*.bmp')] + expected_count = 27 + elif light_bg: + candidates = [str(p) for p in PROJECT_ROOT.join('tests').listdir('sub_red_dark_fg_*.bmp')] + expected_count = 2 + else: + candidates = [str(p) for p in PROJECT_ROOT.join('tests').listdir('sub_red_light_fg_*.bmp')] + expected_count = 2 + assert candidates + + # Run. + with RunNewConsole(command, maximized=True, white_bg=light_bg) as gen: + screenshot_until_match(str(screenshot), 15, candidates, expected_count, gen) diff --git a/tests/test_parse.py b/tests/test_parse.py new file mode 100644 index 0000000..c93d77f --- /dev/null +++ b/tests/test_parse.py @@ -0,0 +1,79 @@ +"""Test objects in module.""" + +import pytest + +from colorclass.parse import parse_input, prune_overridden + + +@pytest.mark.parametrize('in_,expected', [ + ('', ''), + ('test', 'test'), + ('\033[31mTEST\033[0m', '\033[31mTEST\033[0m'), + ('\033[32;31mTEST\033[39;0m', '\033[31mTEST\033[0m'), + ('\033[1;2mTEST\033[22;22m', '\033[1;2mTEST\033[22m'), + ('\033[1;1;1;1;1;1mTEST\033[22m', '\033[1mTEST\033[22m'), + ('\033[31;32;41;42mTEST\033[39;49m', '\033[32;42mTEST\033[39;49m'), +]) +def test_prune_overridden(in_, expected): + """Test function. + + :param str in_: Input string to pass to function. + :param str expected: Expected return value. + """ + actual = prune_overridden(in_) + assert actual == expected + + +@pytest.mark.parametrize('disable', [True, False]) +@pytest.mark.parametrize('in_,expected_colors,expected_no_colors', [ + ('', '', ''), + ('test', 'test', 'test'), + ('{b}TEST{/b}', '\033[1mTEST\033[22m', 'TEST'), + ('{red}{bgred}TEST{/all}', '\033[31;41mTEST\033[0m', 'TEST'), + ('{b}A {red}B {green}{bgred}C {/all}', '\033[1mA \033[31mB \033[32;41mC \033[0m', 'A B C '), + ('C {/all}{b}{blue}{hiblue}{bgcyan}D {/all}', 'C \033[0;1;46;94mD \033[0m', 'C D '), + ('D {/all}{i}\033[31;103mE {/all}', 'D \033[0;3;31;103mE \033[0m', 'D E '), + ('{b}{red}{bgblue}{/all}{i}TEST{/all}', '\033[0;3mTEST\033[0m', 'TEST'), + ('{red}{green}{blue}{black}{yellow}TEST{/fg}{/all}', '\033[33mTEST\033[0m', 'TEST'), + ('{bgred}{bggreen}{bgblue}{bgblack}{bgyellow}TEST{/bg}{/all}', '\033[43mTEST\033[0m', 'TEST'), + ('{red}T{red}E{red}S{red}T{/all}', '\033[31mTEST\033[0m', 'TEST'), + ('{red}T{/all}E{/all}S{/all}T{/all}', '\033[31mT\033[0mEST', 'TEST'), + ('{red}{bgblue}TES{red}{bgblue}T{/all}', '\033[31;44mTEST\033[0m', 'TEST'), +]) +def test_parse_input(disable, in_, expected_colors, expected_no_colors): + """Test function. + + :param bool disable: Disable colors? + :param str in_: Input string to pass to function. + :param str expected_colors: Expected first item of return value. + :param str expected_no_colors: Expected second item of return value. + """ + actual_colors, actual_no_colors = parse_input(in_, disable, False) + if disable: + assert actual_colors == expected_no_colors + else: + assert actual_colors == expected_colors + assert actual_no_colors == expected_no_colors + + +@pytest.mark.parametrize('disable', [True, False]) +@pytest.mark.parametrize('in_,expected_colors,expected_no_colors', [ + ('', '', ''), + ('test', 'test', 'test'), + ('{b}TEST{/b}', '{b}TEST{/b}', '{b}TEST{/b}'), + ('D {/all}{i}\033[31;103mE {/all}', 'D {/all}{i}\033[31;103mE {/all}', 'D {/all}{i}E {/all}'), +]) +def test_parse_input_keep_tags(disable, in_, expected_colors, expected_no_colors): + """Test function with keep_tags=True. + + :param bool disable: Disable colors? + :param str in_: Input string to pass to function. + :param str expected_colors: Expected first item of return value. + :param str expected_no_colors: Expected second item of return value. + """ + actual_colors, actual_no_colors = parse_input(in_, disable, True) + if disable: + assert actual_colors == expected_no_colors + else: + assert actual_colors == expected_colors + assert actual_no_colors == expected_no_colors diff --git a/tests/test_search.py b/tests/test_search.py new file mode 100644 index 0000000..0d5000d --- /dev/null +++ b/tests/test_search.py @@ -0,0 +1,51 @@ +"""Test objects in module.""" + +import pytest + +from colorclass.search import build_color_index, find_char_color + + +@pytest.mark.parametrize('in_,expected', [ + ['', ()], + ['TEST', (0, 1, 2, 3)], + ['!\033[31mRed\033[0m', (0, 6, 7, 8)], + ['\033[1mA \033[31mB \033[32;41mC \033[0mD', (4, 5, 11, 12, 21, 22, 27)], +]) +def test_build_color_index(in_, expected): + """Test function. + + :param str in_: Input string to pass to function. + :param str expected: Expected return value. + """ + actual = build_color_index(in_) + assert actual == expected + + +@pytest.mark.parametrize('in_,pos,expected', [ + ('TEST', 0, 'T'), + + ('\033[31mTEST', 0, '\033[31mT'), + ('\033[31mTEST', 3, '\033[31mT'), + + ('\033[31mT\033[32mE\033[33mS\033[34mT', 0, '\033[31mT\033[32m\033[33m\033[34m'), + ('\033[31mT\033[32mE\033[33mS\033[34mT', 2, '\033[31m\033[32m\033[33mS\033[34m'), + + ('\033[31mTEST\033[0m', 1, '\033[31mE\033[0m'), + ('\033[31mTEST\033[0m', 3, '\033[31mT\033[0m'), + + ('T\033[31mES\033[0mT', 0, 'T\033[31m\033[0m'), + ('T\033[31mES\033[0mT', 1, '\033[31mE\033[0m'), + ('T\033[31mES\033[0mT', 2, '\033[31mS\033[0m'), + ('T\033[31mES\033[0mT', 3, '\033[31m\033[0mT'), +]) +def test_find_char_color(in_, pos, expected): + """Test function. + + :param str in_: Input string to pass to function. + :param int pos: Character position in non-color string to lookup. + :param str expected: Expected return value. + """ + index = build_color_index(in_) + color_pos = index[pos] + actual = find_char_color(in_, color_pos) + assert actual == expected diff --git a/tests/test_toggles.py b/tests/test_toggles.py new file mode 100644 index 0000000..a2a6cda --- /dev/null +++ b/tests/test_toggles.py @@ -0,0 +1,29 @@ +"""Test objects in module.""" + +from colorclass import toggles + + +def test_disable(): + """Test functions.""" + toggles.disable_all_colors() + assert not toggles.is_enabled() + toggles.enable_all_colors() + assert toggles.is_enabled() + toggles.disable_all_colors() + assert not toggles.is_enabled() + toggles.enable_all_colors() + assert toggles.is_enabled() + assert toggles.disable_if_no_tty() # pytest pipes stderr/stdout. + assert not toggles.is_enabled() + + +def test_light_bg(): + """Test functions.""" + toggles.set_dark_background() + assert not toggles.is_light() + toggles.set_light_background() + assert toggles.is_enabled() + toggles.set_dark_background() + assert not toggles.is_light() + toggles.set_light_background() + assert toggles.is_enabled() diff --git a/tests/test_windows.py b/tests/test_windows.py new file mode 100644 index 0000000..e96e4f9 --- /dev/null +++ b/tests/test_windows.py @@ -0,0 +1,429 @@ +"""Test Windows methods.""" + +from __future__ import print_function + +import ctypes +import sys +from textwrap import dedent + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +import pytest + +from colorclass import windows +from colorclass.codes import ANSICodeMapping +from colorclass.color import Color +from tests.conftest import PROJECT_ROOT +from tests.screenshot import RunNewConsole, screenshot_until_match + + +class MockKernel32(object): + """Mock kernel32.""" + + def __init__(self, stderr=windows.INVALID_HANDLE_VALUE, stdout=windows.INVALID_HANDLE_VALUE, set_mode=0x0): + """Constructor.""" + self.set_mode = set_mode + self.stderr = stderr + self.stdout = stdout + self.wAttributes = 7 + + def GetConsoleMode(self, _, dword_pointer): # noqa + """Mock GetConsoleMode. + + :param _: Unused handle. + :param dword_pointer: ctypes.byref(lpdword) return value. + """ + ulong_ptr = ctypes.POINTER(ctypes.c_ulong) + dword = ctypes.cast(dword_pointer, ulong_ptr).contents # Dereference pointer. + dword.value = self.set_mode + return 1 + + def GetConsoleScreenBufferInfo(self, _, csbi_pointer): # noqa + """Mock GetConsoleScreenBufferInfo. + + :param _: Unused handle. + :param csbi_pointer: ctypes.byref(csbi) return value. + """ + struct_ptr = ctypes.POINTER(windows.ConsoleScreenBufferInfo) + csbi = ctypes.cast(csbi_pointer, struct_ptr).contents # Dereference pointer. + csbi.wAttributes = self.wAttributes + return 1 + + def GetStdHandle(self, handle): # noqa + """Mock GetStdHandle. + + :param int handle: STD_ERROR_HANDLE or STD_OUTPUT_HANDLE. + """ + return self.stderr if handle == windows.STD_ERROR_HANDLE else self.stdout + + def SetConsoleTextAttribute(self, _, color_code): # noqa + """Mock SetConsoleTextAttribute. + + :param _: Unused handle. + :param int color_code: Merged color code to set. + """ + self.wAttributes = color_code + return 1 + + +class MockSys(object): + """Mock sys standard library module.""" + + def __init__(self, stderr=None, stdout=None): + """Constructor.""" + self.stderr = stderr or type('', (), {}) + self.stdout = stdout or type('', (), {}) + + +@pytest.mark.skipif(str(not windows.IS_WINDOWS)) +def test_init_kernel32_unique(): + """Make sure function doesn't override other LibraryLoaders.""" + k32_a = ctypes.LibraryLoader(ctypes.WinDLL).kernel32 + k32_a.GetStdHandle.argtypes = [ctypes.c_void_p] + k32_a.GetStdHandle.restype = ctypes.c_ulong + + k32_b, stderr_b, stdout_b = windows.init_kernel32() + + k32_c = ctypes.LibraryLoader(ctypes.WinDLL).kernel32 + k32_c.GetStdHandle.argtypes = [ctypes.c_long] + k32_c.GetStdHandle.restype = ctypes.c_short + + k32_d, stderr_d, stdout_d = windows.init_kernel32() + + # Verify external. + assert k32_a.GetStdHandle.argtypes == [ctypes.c_void_p] + assert k32_a.GetStdHandle.restype == ctypes.c_ulong + assert k32_c.GetStdHandle.argtypes == [ctypes.c_long] + assert k32_c.GetStdHandle.restype == ctypes.c_short + + # Verify ours. + assert k32_b.GetStdHandle.argtypes == [ctypes.c_ulong] + assert k32_b.GetStdHandle.restype == ctypes.c_void_p + assert k32_d.GetStdHandle.argtypes == [ctypes.c_ulong] + assert k32_d.GetStdHandle.restype == ctypes.c_void_p + assert stderr_b == stderr_d + assert stdout_b == stdout_d + + +@pytest.mark.parametrize('stderr_invalid', [False, True]) +@pytest.mark.parametrize('stdout_invalid', [False, True]) +def test_init_kernel32_valid_handle(monkeypatch, stderr_invalid, stdout_invalid): + """Test valid/invalid handle handling. + + :param monkeypatch: pytest fixture. + :param bool stderr_invalid: Mock stderr is valid. + :param bool stdout_invalid: Mock stdout is valid. + """ + mock_sys = MockSys() + monkeypatch.setattr(windows, 'sys', mock_sys) + if stderr_invalid: + setattr(mock_sys.stderr, '_original_stream', True) + if stdout_invalid: + setattr(mock_sys.stdout, '_original_stream', True) + + stderr, stdout = windows.init_kernel32(MockKernel32(stderr=100, stdout=200))[1:] + + if stderr_invalid and stdout_invalid: + assert stderr == windows.INVALID_HANDLE_VALUE + assert stdout == windows.INVALID_HANDLE_VALUE + elif stdout_invalid: + assert stderr == 100 + assert stdout == windows.INVALID_HANDLE_VALUE + elif stderr_invalid: + assert stderr == windows.INVALID_HANDLE_VALUE + assert stdout == 200 + else: + assert stderr == 100 + assert stdout == 200 + + +def test_get_console_info(): + """Test function.""" + # Test error. + if windows.IS_WINDOWS: + with pytest.raises(OSError): + windows.get_console_info(windows.init_kernel32()[0], windows.INVALID_HANDLE_VALUE) + + # Test no error with mock methods. + kernel32 = MockKernel32() + fg_color, bg_color, native_ansi = windows.get_console_info(kernel32, windows.INVALID_HANDLE_VALUE) + assert fg_color == 7 + assert bg_color == 0 + assert native_ansi is False + + # Test different console modes. + for not_native in (0x0, 0x1, 0x2, 0x1 | 0x2): + kernel32.set_mode = not_native + assert not windows.get_console_info(kernel32, windows.INVALID_HANDLE_VALUE)[-1] + for native in (i | 0x4 for i in (0x0, 0x1, 0x2, 0x1 | 0x2)): + kernel32.set_mode = native + assert windows.get_console_info(kernel32, windows.INVALID_HANDLE_VALUE)[-1] + + +@pytest.mark.parametrize('stderr', [1, windows.INVALID_HANDLE_VALUE]) +@pytest.mark.parametrize('stdout', [2, windows.INVALID_HANDLE_VALUE]) +def test_bg_color_native_ansi(stderr, stdout): + """Test function. + + :param int stderr: Value of parameter. + :param int stdout: Value of parameter. + """ + kernel32 = MockKernel32(set_mode=0x4) + kernel32.wAttributes = 240 + actual = windows.bg_color_native_ansi(kernel32, stderr, stdout) + if stderr == windows.INVALID_HANDLE_VALUE and stdout == windows.INVALID_HANDLE_VALUE: + expected = 0, False + else: + expected = 240, True + assert actual == expected + + +def test_windows_stream(): + """Test class.""" + # Test error. + if windows.IS_WINDOWS: + stream = windows.WindowsStream(windows.init_kernel32()[0], windows.INVALID_HANDLE_VALUE, StringIO()) + assert stream.colors == (windows.WINDOWS_CODES['white'], windows.WINDOWS_CODES['black']) + stream.colors = windows.WINDOWS_CODES['red'] | windows.WINDOWS_CODES['bgblue'] # No exception, just ignore. + assert stream.colors == (windows.WINDOWS_CODES['white'], windows.WINDOWS_CODES['black']) + + # Test __getattr__() and color resetting. + original_stream = StringIO() + stream = windows.WindowsStream(MockKernel32(), windows.INVALID_HANDLE_VALUE, original_stream) + assert stream.writelines == original_stream.writelines # Test __getattr__(). + assert stream.colors == (windows.WINDOWS_CODES['white'], windows.WINDOWS_CODES['black']) + stream.colors = windows.WINDOWS_CODES['red'] | windows.WINDOWS_CODES['bgblue'] + assert stream.colors == (windows.WINDOWS_CODES['red'], windows.WINDOWS_CODES['bgblue']) + stream.colors = None # Resets colors to original. + assert stream.colors == (windows.WINDOWS_CODES['white'], windows.WINDOWS_CODES['black']) + + # Test special negative codes. + stream.colors = windows.WINDOWS_CODES['red'] | windows.WINDOWS_CODES['bgblue'] + stream.colors = windows.WINDOWS_CODES['/fg'] + assert stream.colors == (windows.WINDOWS_CODES['white'], windows.WINDOWS_CODES['bgblue']) + stream.colors = windows.WINDOWS_CODES['red'] | windows.WINDOWS_CODES['bgblue'] + stream.colors = windows.WINDOWS_CODES['/bg'] + assert stream.colors == (windows.WINDOWS_CODES['red'], windows.WINDOWS_CODES['black']) + stream.colors = windows.WINDOWS_CODES['red'] | windows.WINDOWS_CODES['bgblue'] + stream.colors = windows.WINDOWS_CODES['bgblack'] + assert stream.colors == (windows.WINDOWS_CODES['red'], windows.WINDOWS_CODES['black']) + + # Test write. + stream.write(Color('{/all}A{red}B{bgblue}C')) + original_stream.seek(0) + assert original_stream.read() == 'ABC' + assert stream.colors == (windows.WINDOWS_CODES['red'], windows.WINDOWS_CODES['bgblue']) + + # Test ignore invalid code. + original_stream.seek(0) + original_stream.truncate() + stream.write('\x1b[0mA\x1b[31mB\x1b[44;999mC') + original_stream.seek(0) + assert original_stream.read() == 'ABC' + assert stream.colors == (windows.WINDOWS_CODES['red'], windows.WINDOWS_CODES['bgblue']) + + +@pytest.mark.skipif(str(windows.IS_WINDOWS)) +def test_windows_nix(): + """Test enable/disable on non-Windows platforms.""" + with windows.Windows(): + assert not windows.Windows.is_enabled() + assert not hasattr(sys.stderr, '_original_stream') + assert not hasattr(sys.stdout, '_original_stream') + assert not windows.Windows.is_enabled() + assert not hasattr(sys.stderr, '_original_stream') + assert not hasattr(sys.stdout, '_original_stream') + + +def test_windows_auto_colors(monkeypatch): + """Test Windows class with/out valid_handle and with/out auto_colors. Don't replace streams. + + :param monkeypatch: pytest fixture. + """ + mock_sys = MockSys() + monkeypatch.setattr(windows, 'atexit', type('', (), {'register': staticmethod(lambda _: 0 / 0)})) + monkeypatch.setattr(windows, 'IS_WINDOWS', True) + monkeypatch.setattr(windows, 'sys', mock_sys) + monkeypatch.setattr(ANSICodeMapping, 'LIGHT_BACKGROUND', None) + + # Test no valid handles. + kernel32 = MockKernel32() + monkeypatch.setattr(windows, 'init_kernel32', lambda: (kernel32, -1, -1)) + assert not windows.Windows.enable() + assert not windows.Windows.is_enabled() + assert not hasattr(mock_sys.stderr, '_original_stream') + assert not hasattr(mock_sys.stdout, '_original_stream') + assert ANSICodeMapping.LIGHT_BACKGROUND is None + + # Test auto colors dark background. + kernel32.set_mode = 0x4 # Enable native ANSI to have Windows skip replacing streams. + monkeypatch.setattr(windows, 'init_kernel32', lambda: (kernel32, 1, 2)) + assert not windows.Windows.enable(auto_colors=True) + assert not windows.Windows.is_enabled() + assert not hasattr(mock_sys.stderr, '_original_stream') + assert not hasattr(mock_sys.stdout, '_original_stream') + assert ANSICodeMapping.LIGHT_BACKGROUND is False + + # Test auto colors light background. + kernel32.wAttributes = 240 + assert not windows.Windows.enable(auto_colors=True) + assert not windows.Windows.is_enabled() + assert not hasattr(mock_sys.stderr, '_original_stream') + assert not hasattr(mock_sys.stdout, '_original_stream') + assert ANSICodeMapping.LIGHT_BACKGROUND is True + + +@pytest.mark.parametrize('valid', ['stderr', 'stdout', 'both']) +def test_windows_replace_streams(monkeypatch, tmpdir, valid): + """Test Windows class stdout and stderr replacement. + + :param monkeypatch: pytest fixture. + :param tmpdir: pytest fixture. + :param str valid: Which mock stream(s) should be valid. + """ + ac = list() # atexit called. + mock_sys = MockSys(stderr=tmpdir.join('stderr').open(mode='wb'), stdout=tmpdir.join('stdout').open(mode='wb')) + monkeypatch.setattr(windows, 'atexit', type('', (), {'register': staticmethod(lambda _: ac.append(1))})) + monkeypatch.setattr(windows, 'IS_WINDOWS', True) + monkeypatch.setattr(windows, 'sys', mock_sys) + + # Mock init_kernel32. + stderr = 1 if valid in ('stderr', 'both') else windows.INVALID_HANDLE_VALUE + stdout = 2 if valid in ('stdout', 'both') else windows.INVALID_HANDLE_VALUE + monkeypatch.setattr(windows, 'init_kernel32', lambda: (MockKernel32(), stderr, stdout)) + + # Test. + assert windows.Windows.enable(reset_atexit=True) + assert windows.Windows.is_enabled() + assert len(ac) == 1 + if stderr != windows.INVALID_HANDLE_VALUE: + assert hasattr(mock_sys.stderr, '_original_stream') + else: + assert not hasattr(mock_sys.stderr, '_original_stream') + if stdout != windows.INVALID_HANDLE_VALUE: + assert hasattr(mock_sys.stdout, '_original_stream') + else: + assert not hasattr(mock_sys.stdout, '_original_stream') + + # Test multiple disable. + assert windows.Windows.disable() + assert not windows.Windows.is_enabled() + assert not windows.Windows.disable() + assert not windows.Windows.is_enabled() + + # Test context manager. + with windows.Windows(): + assert windows.Windows.is_enabled() + assert not windows.Windows.is_enabled() + + +@pytest.mark.skipif(str(not windows.IS_WINDOWS)) +def test_enable_disable(tmpdir): + """Test enabling, disabling, repeat. Make sure colors still work. + + :param tmpdir: pytest fixture. + """ + screenshot = PROJECT_ROOT.join('test_windows_test_enable_disable.png') + if screenshot.check(): + screenshot.remove() + script = tmpdir.join('script.py') + command = [sys.executable, str(script)] + + script.write(dedent("""\ + from __future__ import print_function + import os, time + from colorclass import Color, Windows + + with Windows(auto_colors=True): + print(Color('{autored}Red{/autored}')) + print('Red') + with Windows(auto_colors=True): + print(Color('{autored}Red{/autored}')) + print('Red') + + stop_after = time.time() + 20 + while not os.path.exists(r'%s') and time.time() < stop_after: + time.sleep(0.5) + """) % str(screenshot)) + + # Setup expected. + with_colors = [str(p) for p in PROJECT_ROOT.join('tests').listdir('sub_red_light_fg_*.bmp')] + sans_colors = [str(p) for p in PROJECT_ROOT.join('tests').listdir('sub_red_sans_*.bmp')] + assert with_colors + assert sans_colors + + # Run. + with RunNewConsole(command) as gen: + screenshot_until_match(str(screenshot), 15, with_colors, 2, gen) + screenshot_until_match(str(screenshot), 15, sans_colors, 2, gen) + + +@pytest.mark.skipif(str(not windows.IS_WINDOWS)) +def test_box_characters(tmpdir): + """Test for unicode errors with special characters. + + :param tmpdir: pytest fixture. + """ + screenshot = PROJECT_ROOT.join('test_windows_test_box_characters.png') + if screenshot.check(): + screenshot.remove() + script = tmpdir.join('script.py') + command = [sys.executable, str(script)] + + script.write(dedent("""\ + from __future__ import print_function + import os, time + from colorclass import Color, Windows + + Windows.enable(auto_colors=True) + chars = [ + '+', '-', '|', + b'\\xb3'.decode('ibm437'), + b'\\xb4'.decode('ibm437'), + b'\\xb9'.decode('ibm437'), + b'\\xba'.decode('ibm437'), + b'\\xbb'.decode('ibm437'), + b'\\xbc'.decode('ibm437'), + b'\\xbf'.decode('ibm437'), + b'\\xc0'.decode('ibm437'), + b'\\xc1'.decode('ibm437'), + b'\\xc2'.decode('ibm437'), + b'\\xc3'.decode('ibm437'), + b'\\xc4'.decode('ibm437'), + b'\\xc5'.decode('ibm437'), + b'\\xc8'.decode('ibm437'), + b'\\xc9'.decode('ibm437'), + b'\\xca'.decode('ibm437'), + b'\\xcb'.decode('ibm437'), + b'\\xcc'.decode('ibm437'), + b'\\xcd'.decode('ibm437'), + b'\\xce'.decode('ibm437'), + b'\\xd9'.decode('ibm437'), + b'\\xda'.decode('ibm437'), + ] + + for c in chars: + print(c, end='') + print() + for c in chars: + print(Color.green(c, auto=True), end='') + print() + + stop_after = time.time() + 20 + while not os.path.exists(r'%s') and time.time() < stop_after: + time.sleep(0.5) + """) % str(screenshot)) + + # Setup expected. + with_colors = [str(p) for p in PROJECT_ROOT.join('tests').listdir('sub_box_green_*.bmp')] + sans_colors = [str(p) for p in PROJECT_ROOT.join('tests').listdir('sub_box_sans_*.bmp')] + assert with_colors + assert sans_colors + + # Run. + with RunNewConsole(command) as gen: + screenshot_until_match(str(screenshot), 15, with_colors, 1, gen) + screenshot_until_match(str(screenshot), 15, sans_colors, 1, gen) |