219 lines
6.8 KiB
Python
219 lines
6.8 KiB
Python
from __future__ import print_function
|
|
from __future__ import absolute_import
|
|
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 reseverd 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'
|