Source code for tests.ui_tests

#!/usr/bin/env python3
"""Tests for the user interface."""
#
# (C) Pywikibot team, 2008-2022
#
# Distributed under the terms of the MIT license.
#
from __future__ import annotations

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), f'\n{stderrlines[-1]!r} does not end with {end_str!r}')
[docs] class TestTerminalInput(UITestCase): """Terminal input tests.""" input_choice_output = 'question ([A]nswer 1, a[n]swer 2, an[s]wer 3): '
[docs] def testInput(self): self.strin.write('input to read\n') self.strin.seek(0) returned = pywikibot.input('question') self.assertEqual(self.strout.getvalue(), '') self.assertEqual(self.strerr.getvalue(), 'question: ') self.assertIsInstance(returned, str) self.assertEqual(returned, 'input to read')
[docs] def test_input_yn(self): self.strin.write('\n') self.strin.seek(0) returned = pywikibot.input_yn('question', False, automatic_quit=False) self.assertEqual(self.strout.getvalue(), '') self.assertEqual(self.strerr.getvalue(), 'question ([y]es, [N]o): ') self.assertFalse(returned)
def _call_input_choice(self): rv = pywikibot.input_choice( 'question', (('answer 1', 'A'), ('answer 2', 'N'), ('answer 3', 'S')), 'A', automatic_quit=False) self.assertEqual(self.strout.getvalue(), '') self.assertIsInstance(rv, str) return rv
[docs] def testInputChoiceDefault(self): self.strin.write('\n') self.strin.seek(0) returned = self._call_input_choice() self.assertEqual(returned, 'a')
[docs] def testInputChoiceCapital(self): self.strin.write('N\n') self.strin.seek(0) returned = self._call_input_choice() self.assertEqual(self.strerr.getvalue(), self.input_choice_output) self.assertEqual(returned, 'n')
[docs] def testInputChoiceNonCapital(self): self.strin.write('n\n') self.strin.seek(0) returned = self._call_input_choice() self.assertEqual(self.strerr.getvalue(), self.input_choice_output) self.assertEqual(returned, 'n')
[docs] def testInputChoiceIncorrectAnswer(self): self.strin.write('X\nN\n') self.strin.seek(0) returned = self._call_input_choice() self.assertEqual(self.strerr.getvalue(), self.input_choice_output * 2) self.assertEqual(returned, 'n')
[docs] def test_input_list_choice(self): """Test input_list_choice function.""" options = ('answer 1', 'answer 2', 'answer 3') rv = pywikibot.bot.input_list_choice('question', options, '2') self.assertEqual(self.strout.getvalue(), '') self.assertEqual( self.strerr.getvalue(), ''.join(f'{num}: {items}\n' for num, items in enumerate(options, start=1)) + 'question (default: 2): ') self.assertEqual(rv, 'answer 2')
[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, f'{char!r} not in transliteration table')
[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__': with suppress(SystemExit): unittest.main()