#!/usr/bin/env python3
"""Tests for the user interface."""
#
# (C) Pywikibot team, 2008-2022
#
# Distributed under the terms of the MIT license.
#
import io
import logging
import os
import unittest
from contextlib import redirect_stdout, suppress
from unittest.mock import patch
import pywikibot
from pywikibot.bot import (
CRITICAL,
DEBUG,
ERROR,
INFO,
INPUT,
STDOUT,
VERBOSE,
WARNING,
)
from pywikibot.userinterfaces import (
terminal_interface_base,
terminal_interface_unix,
terminal_interface_win32,
)
from pywikibot.userinterfaces.transliteration import NON_LATIN_DIGITS, _trans
from tests.aspects import TestCase, TestCaseBase
logger = logging.getLogger('pywiki')
loggingcontext = {'caller_name': 'ui_tests',
'caller_file': 'ui_tests',
'caller_line': 0,
'newline': '\n'}
[docs]class UITestCase(TestCaseBase):
"""UI tests."""
net = False
[docs] def setUp(self):
"""Setup test.
Here we patch standard input, output, and errors, essentially
redirecting to `StringIO` streams.
"""
super().setUp()
self.stdout_patcher = patch('sys.stdout', new_callable=io.StringIO)
self.strout = self.stdout_patcher.start()
self.stderr_patcher = patch('sys.stderr', new_callable=io.StringIO)
self.strerr = self.stderr_patcher.start()
self.stdin_patcher = patch('sys.stdin', new_callable=io.StringIO)
self.strin = self.stdin_patcher.start()
pywikibot.bot.set_interface('terminal')
self.org_input = pywikibot.bot.ui._raw_input
pywikibot.bot.ui._raw_input = self._patched_input
pywikibot.config.colorized_output = True
pywikibot.config.transliterate = False
pywikibot.ui.transliteration_target = None
pywikibot.ui.encoding = 'utf-8'
[docs] def tearDown(self):
"""Cleanup test."""
super().tearDown()
self.stdout_patcher.stop()
self.stderr_patcher.stop()
self.stdin_patcher.stop()
pywikibot.bot.ui._raw_input = self.org_input
pywikibot.bot.set_interface('buffer')
def _patched_input(self):
return self.strin.readline().strip()
[docs]class ExceptionTestError(Exception):
"""Test exception."""
[docs]class TestTerminalOutput(UITestCase):
"""Terminal output tests."""
tests = [
('debug', DEBUG, '', ''),
('verbose', VERBOSE, '', ''),
('info', INFO, '', 'info\n'),
('stdout', STDOUT, 'stdout\n', ''),
('input', INPUT, '', 'input\n'),
('WARNING', WARNING, '', 'WARNING: WARNING\n'),
('ERROR', ERROR, '', 'ERROR: ERROR\n'),
('CRITICAL', CRITICAL, '', 'CRITICAL: CRITICAL\n'),
]
[docs] def test_outputlevels_logging(self):
"""Test logger with output levels."""
for text, level, out, err in self.tests:
with self.subTest(test=text):
logger.log(level, text, extra=loggingcontext)
self.assertEqual(self.strout.getvalue(), out)
self.assertEqual(self.strerr.getvalue(), err)
# reset terminal files
for stream in [self.strout, self.strerr, self.strin]:
stream.truncate(0)
stream.seek(0)
[docs] def test_output(self):
pywikibot.info('output')
self.assertEqual(self.strout.getvalue(), '')
self.assertEqual(self.strerr.getvalue(), 'output\n')
[docs] def test_stdout(self):
pywikibot.stdout('output')
self.assertEqual(self.strout.getvalue(), 'output\n')
self.assertEqual(self.strerr.getvalue(), '')
[docs] def test_warning(self):
pywikibot.warning('warning')
self.assertEqual(self.strout.getvalue(), '')
self.assertEqual(self.strerr.getvalue(), 'WARNING: warning\n')
[docs] def test_error(self):
pywikibot.error('error')
self.assertEqual(self.strout.getvalue(), '')
self.assertEqual(self.strerr.getvalue(), 'ERROR: error\n')
[docs] def test_log(self):
pywikibot.log('log')
self.assertEqual(self.strout.getvalue(), '')
self.assertEqual(self.strerr.getvalue(), '')
[docs] def test_critical(self):
pywikibot.critical('critical')
self.assertEqual(self.strout.getvalue(), '')
self.assertEqual(self.strerr.getvalue(), 'CRITICAL: critical\n')
[docs] def test_debug(self):
pywikibot.debug('debug', layer='test')
self.assertEqual(self.strout.getvalue(), '')
self.assertEqual(self.strerr.getvalue(), '')
[docs] def test_exception(self):
try:
raise ExceptionTestError('Testing Exception')
except ExceptionTestError:
pywikibot.error('exception', exc_info=False)
self.assertEqual(self.strout.getvalue(), '')
self.assertEqual(self.strerr.getvalue(),
'ERROR: exception\n')
[docs] def test_exception_empty(self):
try:
raise ExceptionTestError('Testing Exception')
except ExceptionTestError:
pywikibot.exception(exc_info=False)
self.assertEqual(self.strout.getvalue(), '')
self.assertEqual(self.strerr.getvalue(),
'ERROR: Testing Exception (ExceptionTestError)\n')
[docs] def test_exception_tb(self):
try:
raise ExceptionTestError('Testing Exception')
except ExceptionTestError:
pywikibot.exception()
self.assertEqual(self.strout.getvalue(), '')
stderrlines = self.strerr.getvalue().split('\n')
self.assertEqual(stderrlines[0],
'ERROR: Testing Exception')
self.assertEqual(stderrlines[1], 'Traceback (most recent call last):')
self.assertEqual(stderrlines[3],
" raise ExceptionTestError('Testing Exception')")
end_str = ': Testing Exception'
self.assertTrue(stderrlines[-1].endswith(end_str),
'\n{!r} does not end with {!r}'
.format(stderrlines[-1], end_str))
[docs]@unittest.skipUnless(os.name == 'posix', 'requires Unix console')
class TestTerminalOutputColorUnix(UITestCase):
"""Terminal output color tests."""
str1 = 'text <<lightpurple>>light purple text<<default>> text'
[docs] def testOutputColorizedText(self):
pywikibot.info(self.str1)
self.assertEqual(self.strout.getvalue(), '')
self.assertEqual(
self.strerr.getvalue(),
'text \x1b[95mlight purple text\x1b[0m text\n')
[docs] def testOutputNoncolorizedText(self):
pywikibot.config.colorized_output = False
pywikibot.info(self.str1)
self.assertEqual(self.strout.getvalue(), '')
self.assertEqual(
self.strerr.getvalue(),
'text light purple text text ***\n')
str2 = ('normal text <<lightpurple>> light purple '
'<<lightblue>> light blue <<previous>> light purple '
'<<default>> normal text')
[docs] def testOutputColorCascade_incorrect(self):
"""Test incorrect behavior of testOutputColorCascade."""
pywikibot.info(self.str2)
self.assertEqual(self.strout.getvalue(), '')
self.assertEqual(
self.strerr.getvalue(),
'normal text \x1b[95m light purple '
'\x1b[94m light blue \x1b[95m light purple '
'\x1b[0m normal text\n')
[docs]@unittest.skipUnless(os.name == 'posix', 'requires Unix console')
class TestTerminalUnicodeUnix(UITestCase):
"""Terminal output tests for Unix."""
[docs] def testOutputUnicodeText(self):
pywikibot.info('Заглавная_страница')
self.assertEqual(self.strout.getvalue(), '')
self.assertEqual(self.strerr.getvalue(), 'Заглавная_страница\n')
[docs] def testInputUnicodeText(self):
self.strin.write('Заглавная_страница\n')
self.strin.seek(0)
returned = pywikibot.input('Википедию? ')
self.assertEqual(self.strout.getvalue(), '')
self.assertEqual(
self.strerr.getvalue(), 'Википедию? ')
self.assertIsInstance(returned, str)
self.assertEqual(returned, 'Заглавная_страница')
[docs]@unittest.skipUnless(os.name == 'posix', 'requires Unix console')
class TestTransliterationUnix(UITestCase):
"""Terminal output transliteration tests."""
[docs] def testOutputTransliteratedUnicodeText(self):
pywikibot.bot.ui.encoding = 'latin-1'
pywikibot.config.transliterate = True
pywikibot.info('abcd АБГД αβγδ あいうえお')
self.assertEqual(self.strout.getvalue(), '')
self.assertEqual(
self.strerr.getvalue(),
'abcd \x1b[93mA\x1b[0m\x1b[93mB\x1b[0m\x1b[93mG\x1b[0m'
'\x1b[93mD\x1b[0m \x1b[93ma\x1b[0m\x1b[93mb\x1b[0m\x1b[93mg'
'\x1b[0m\x1b[93md\x1b[0m \x1b[93ma\x1b[0m\x1b[93mi\x1b[0m'
'\x1b[93mu\x1b[0m\x1b[93me\x1b[0m\x1b[93mo\x1b[0m\n')
[docs]class TestTransliterationTable(TestCase):
"""Test transliteration table."""
net = False
[docs] def test_latin_digits(self):
"""Test that non latin digits are in transliteration table."""
for lang, digits in NON_LATIN_DIGITS.items():
with self.subTest(lang=lang):
for char in digits:
self.assertIn(char, _trans,
'{!r} not in transliteration table'
.format(char))
[docs] def test_transliteration_table(self):
"""Test transliteration table consistency."""
for k, v in _trans.items():
with self.subTest():
self.assertNotEqual(k, v)
# TODO: add tests for background colors.
[docs]class FakeUITest(TestCase):
"""Test case to allow doing uncolorized general UI tests."""
net = False
expected = 'Hello world you! ***'
expect_color = False
ui_class = terminal_interface_base.UI
[docs] def setUp(self):
"""Create dummy instances for the test and patch encounter_color."""
super().setUp()
self.ui_obj = self.ui_class()
# Write to sys.stdout stream, which we'll redirect to the stream below
self.redirect = io.StringIO()
self._orig_encounter_color = self.ui_obj.encounter_color
self.ui_obj.encounter_color = self._encounter_color
self._index = 0
[docs] def tearDown(self):
"""Unpatch the encounter_color method."""
self.ui_obj.encounter_color = self._orig_encounter_color
super().tearDown()
self.assertEqual(self._index,
len(self._colors) if self.expect_color else 0)
def _encounter_color(self, color, target_stream):
"""Patched encounter_color method."""
raise AssertionError(
'This method should not be invoked') # pragma: no cover
[docs] def test_no_color(self):
"""Test a string without any colors."""
self._colors = ()
with redirect_stdout(self.redirect) as f:
self.ui_obj._print('Hello world you!', self.ui_obj.stdout)
self.assertEqual(f.getvalue(), 'Hello world you!')
[docs] def test_one_color(self):
"""Test a string using one color."""
self._colors = (('red', 6), ('default', 10))
with redirect_stdout(self.redirect) as f:
self.ui_obj._print('Hello <<red>>world you!', self.ui_obj.stdout)
self.assertEqual(f.getvalue(), self.expected)
[docs] def test_flat_color(self):
"""Test using colors with defaulting in between."""
self._colors = (('red', 6), ('default', 6), ('yellow', 3),
('default', 1))
with redirect_stdout(self.redirect) as f:
self.ui_obj._print(
'Hello <<red>>world <<default>>you<<yellow>>!',
self.ui_obj.stdout)
self.assertEqual(f.getvalue(), self.expected)
[docs] def test_stack_with_pop_color(self):
"""Test using stacked colors and just popping the latest color."""
self._colors = (('red', 6), ('yellow', 6), ('red', 3), ('default', 1))
with redirect_stdout(self.redirect) as f:
self.ui_obj._print(
'Hello <<red>>world <<yellow>>you<<previous>>!',
self.ui_obj.stdout)
self.assertEqual(f.getvalue(), self.expected)
[docs] def test_stack_implicit_color(self):
"""Test using stacked colors without popping any."""
self._colors = (('red', 6), ('yellow', 6), ('default', 4))
with redirect_stdout(self.redirect) as f:
self.ui_obj._print('Hello <<red>>world <<yellow>>you!',
self.ui_obj.stdout)
self.assertEqual(f.getvalue(), self.expected)
[docs] def test_one_color_newline(self):
"""Test with trailing new line and one color."""
self._colors = (('red', 6), ('default', 11))
with redirect_stdout(self.redirect) as f:
self.ui_obj._print('Hello <<red>>world you!\n',
self.ui_obj.stdout)
self.assertEqual(f.getvalue(), self.expected + '\n')
[docs]class FakeUIColorizedTestBase(TestCase):
"""Base class for test cases requiring that colorized output is active."""
net = False
expect_color = True
expected = 'Hello world you!'
[docs] def setUp(self):
"""Force colorized_output to True."""
super().setUp()
self._old_config = pywikibot.config.colorized_output
pywikibot.config.colorized_output = True
[docs] def tearDown(self):
"""Undo colorized_output configuration."""
pywikibot.config.colorized_output = self._old_config
super().tearDown()
def _encounter_color(self, color, target_stream):
"""Verify that the written data, color and stream are correct."""
self.assertIs(target_stream, self.ui_obj.stdout)
expected_color = self._colors[self._index][0]
self._index += 1
self.assertEqual(color, expected_color)
self.assertLength(self.redirect.getvalue(),
sum(e[1] for e in self._colors[:self._index]))
[docs]class FakeUnixTest(FakeUIColorizedTestBase, FakeUITest):
"""Test case to allow doing colorized Unix tests in any environment."""
ui_class = terminal_interface_unix.UnixUI
[docs]class FakeWin32Test(FakeUIColorizedTestBase, FakeUITest):
"""
Test case to allow doing colorized Win32 tests in any environment.
This only patches the ctypes import in the terminal_interface_win32
module. As the Win32CtypesUI is using the std-streams from another
import these will be unpatched.
"""
ui_class = terminal_interface_win32.Win32UI
[docs] def setUp(self):
"""Patch the ctypes import and initialize a stream and UI instance."""
super().setUp()
self.ui_obj.stdout.isatty = lambda: self.expect_color
[docs]class FakeWin32UncolorizedTest(FakeWin32Test):
"""Test case to allow doing uncolorized Win32 tests in any environment."""
expected = 'Hello world you! ***'
expect_color = False
if __name__ == '__main__': # pragma: no cover
with suppress(SystemExit):
unittest.main()