summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2022-09-16 09:10:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2022-09-16 09:10:14 +0000
commit896739353a613f23c007d9acaa2809010a522a37 (patch)
treecadd194400c11d0a5caaeda7d9d771602eb1ba40 /tests
parentInitial commit. (diff)
downloadcolorclass-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__.py1
-rw-r--r--tests/conftest.py78
-rw-r--r--tests/screenshot.py299
-rw-r--r--tests/sub_box_green_win10.bmpbin0 -> 5026 bytes
-rw-r--r--tests/sub_box_green_winxp.bmpbin0 -> 7254 bytes
-rw-r--r--tests/sub_box_sans_win10.bmpbin0 -> 5026 bytes
-rw-r--r--tests/sub_box_sans_winxp.bmpbin0 -> 7254 bytes
-rw-r--r--tests/sub_red_dark_fg_win10.bmpbin0 -> 446 bytes
-rw-r--r--tests/sub_red_dark_fg_winxp.bmpbin0 -> 702 bytes
-rw-r--r--tests/sub_red_light_fg_win10.bmpbin0 -> 418 bytes
-rw-r--r--tests/sub_red_light_fg_winxp.bmpbin0 -> 702 bytes
-rw-r--r--tests/sub_red_sans_win10.bmpbin0 -> 446 bytes
-rw-r--r--tests/sub_red_sans_winxp.bmpbin0 -> 882 bytes
-rw-r--r--tests/test___main__.py64
-rw-r--r--tests/test_codes.py137
-rw-r--r--tests/test_color.py185
-rw-r--r--tests/test_core.py398
-rw-r--r--tests/test_example.py96
-rw-r--r--tests/test_parse.py79
-rw-r--r--tests/test_search.py51
-rw-r--r--tests/test_toggles.py29
-rw-r--r--tests/test_windows.py429
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
new file mode 100644
index 0000000..485788e
--- /dev/null
+++ b/tests/sub_box_green_win10.bmp
Binary files differ
diff --git a/tests/sub_box_green_winxp.bmp b/tests/sub_box_green_winxp.bmp
new file mode 100644
index 0000000..0823dd0
--- /dev/null
+++ b/tests/sub_box_green_winxp.bmp
Binary files differ
diff --git a/tests/sub_box_sans_win10.bmp b/tests/sub_box_sans_win10.bmp
new file mode 100644
index 0000000..48344b9
--- /dev/null
+++ b/tests/sub_box_sans_win10.bmp
Binary files differ
diff --git a/tests/sub_box_sans_winxp.bmp b/tests/sub_box_sans_winxp.bmp
new file mode 100644
index 0000000..8277d1e
--- /dev/null
+++ b/tests/sub_box_sans_winxp.bmp
Binary files differ
diff --git a/tests/sub_red_dark_fg_win10.bmp b/tests/sub_red_dark_fg_win10.bmp
new file mode 100644
index 0000000..4d66fa9
--- /dev/null
+++ b/tests/sub_red_dark_fg_win10.bmp
Binary files differ
diff --git a/tests/sub_red_dark_fg_winxp.bmp b/tests/sub_red_dark_fg_winxp.bmp
new file mode 100644
index 0000000..2846c5a
--- /dev/null
+++ b/tests/sub_red_dark_fg_winxp.bmp
Binary files differ
diff --git a/tests/sub_red_light_fg_win10.bmp b/tests/sub_red_light_fg_win10.bmp
new file mode 100644
index 0000000..43229a3
--- /dev/null
+++ b/tests/sub_red_light_fg_win10.bmp
Binary files differ
diff --git a/tests/sub_red_light_fg_winxp.bmp b/tests/sub_red_light_fg_winxp.bmp
new file mode 100644
index 0000000..e930459
--- /dev/null
+++ b/tests/sub_red_light_fg_winxp.bmp
Binary files differ
diff --git a/tests/sub_red_sans_win10.bmp b/tests/sub_red_sans_win10.bmp
new file mode 100644
index 0000000..738c172
--- /dev/null
+++ b/tests/sub_red_sans_win10.bmp
Binary files differ
diff --git a/tests/sub_red_sans_winxp.bmp b/tests/sub_red_sans_winxp.bmp
new file mode 100644
index 0000000..a9f0f2e
--- /dev/null
+++ b/tests/sub_red_sans_winxp.bmp
Binary files differ
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)