430 lines
16 KiB
Python
430 lines
16 KiB
Python
|
"""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)
|