debian-koji/tests/test_cli/utils.py
Yuming Zhu ca05418fb5 unittest: use unittest.mock instead of mock
because the absence of unittest.mock on python2.7, we still fallback to
mock
2024-10-23 16:35:30 +00:00

222 lines
6.9 KiB
Python

from __future__ import print_function
from __future__ import absolute_import
try:
from unittest import mock
except ImportError:
import mock
import os
import six
import sys
from six.moves import map
import unittest
PROGNAME = os.path.basename(sys.argv[0]) or 'koji'
"""
Classes
"""
#
# Dummy Mock class
#
class _dummy_(object):
def __enter__(self):
return self
def __exit__(self, *arg, **kwargs):
pass
def assert_called_once(self, *arg, **kwargs):
pass
def assert_called_once_with(self, *arg, **kwargs):
pass
#
# CLI TestCase
#
class CliTestCase(unittest.TestCase):
# public attribute
progname = PROGNAME
error_format = None
STDOUT = sys.stdout
STDERR = sys.stderr
#
# private methods
#
def __assert_callable(self, obj):
if not callable(obj):
raise ValueError('%s is not callable' %
getattr(obj, "__name__", 'function'))
#
# public methods
#
def format_error_message(self, error_message, progname=None):
return self.error_format.format(message=error_message) \
if self.error_format else error_message
def print_message(self, *args, **kwargs):
"""Print message on sys.stdout
This function will not be influenced when sys.stdout is mocked.
"""
kwargs['file'] = self.STDOUT
print(" ".join(map(str, args)), **kwargs)
def assert_function_wrapper(self, callableObj, *args, **kwargs):
"""Wrapper func with anonymous funtion without argument"""
self.__assert_callable(callableObj)
return lambda: callableObj(*args, **kwargs)
def assert_console_message(
self, device, message, wipe=True, regex=False):
# don't care condition
if message is None:
return
output = device.getvalue()
if not regex:
self.assertMultiLineEqual(output, message)
else:
six.assertRegex(self, output, message)
if wipe:
device.seek(0)
device.truncate(0)
def assert_system_exit(self, callableObj, *args, **kwargs):
"""Test if SystemExit exception is issued
Arguments:
callableObj: the test function
*args: Vaiable length arguments that will be passed to callableObj
**kwargs: keyword arguments (see below)
Keyword arguments: (reseverd for assert_system_exit)
activate_session (string):
Mock koji_cli.commands.activate_session and test if it is
called.
Default is on, use None to stop mocking.
stdout:
stderr: Arguments for messages comparison on stdout/stderr. These
arguments allows different data types.
``None`` type will skip message comparison.
``string`` type will do multiple line comparison.
``dict`` type is an advanced type to allow regular expression
format. the format is::
{
'message': 'message string or regular expression',
'wipe': 'wipe the output device, default is True',
'regex': 'True if message format is regular expression'
}
assert_func:
Callable object with no arguments for customized tests.
exit_code:
The exit code when SystemExit is raised
Important! all the other keyword arguments that are not listed
above will be passed to callableObj
"""
# check callableObj callable
self.__assert_callable(callableObj)
# these arguments are reserved and used in assert_system_exit
reserved = [
'activate_session', 'stdout', 'stderr',
'assert_func', 'exit_code'
]
activate = kwargs.get(
'activate_session', 'koji_cli.commands.activate_session')
# stdout/stderr message comparison, None means don't care
# message/error allows many different data types, None, string and dict
message = {}
for key in ['stdout', 'stderr']:
data = kwargs.get(key, None)
message[key] = {'message': None, 'wipe': True, 'regex': False}
if data is None or isinstance(data, six.string_types):
message[key]['message'] = data
elif isinstance(data, dict):
message[key] = {
'message': data.get('message', None),
'wipe': data.get('wipe', True),
'regex': data.get('regex', False),
}
else:
raise ValueError('Invalid data type for %s' % key)
assert_function = kwargs.get(
'assert_func', lambda *args, **kwargs: True)
exit_code = kwargs.get('exit_code', 2)
# args for testee
test_args = args
# kwargs for testee, excludes those that are used in assert_system_exit
test_kwargs = dict((k, v) for k, v in kwargs.items()
if k not in reserved)
# check activate_session must be type of None or string
if activate and not isinstance(activate, six.string_types):
raise ValueError('activate_session is not a string')
session_patch = mock.patch(activate) if activate else _dummy_()
stdout_patch = mock.patch('sys.stdout', new_callable=six.StringIO)
stderr_patch = mock.patch('sys.stderr', new_callable=six.StringIO)
with session_patch as session:
with stdout_patch as stdout:
with stderr_patch as stderr:
with self.assertRaises(SystemExit) as ex:
callableObj(*test_args, **test_kwargs)
self.assertExitCode(ex, exit_code)
session.assert_called_once()
self.assert_console_message(stdout, **message['stdout'])
self.assert_console_message(stderr, **message['stderr'])
assert_function()
@mock.patch('koji_cli.commands.activate_session')
def assert_help(self, callableObj, message, activate_session_mock):
# optarse uses gettext directly and it is driven by LANGUAGE
# we need english to get comparable strings
os.environ['LANGUAGE'] = 'C'
self.assert_system_exit(
callableObj,
mock.MagicMock(),
mock.MagicMock(),
['--help'],
stdout=message,
stderr='',
activate_session=None,
exit_code=0)
activate_session_mock.assert_not_called()
def assertExitCode(self, ex, code):
if isinstance(ex.exception, int):
self.assertEqual(ex.exception, code)
else:
self.assertEqual(ex.exception.code, code)
def get_builtin_open():
if six.PY2:
return '__builtin__.open'
else:
return 'builtins.open'