
# Copyright 2013 Red Hat, Inc.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import sys

import six
import testscenarios

from oslo import messaging
from oslo.messaging._drivers import common as exceptions
from oslo.messaging.openstack.common import jsonutils
from tests import utils as test_utils

load_tests = testscenarios.load_tests_apply_scenarios

EXCEPTIONS_MODULE = 'exceptions' if six.PY2 else 'builtins'


class NovaStyleException(Exception):

    format = 'I am Nova'

    def __init__(self, message=None, **kwargs):
        self.kwargs = kwargs
        if not message:
            message = self.format % kwargs
        super(NovaStyleException, self).__init__(message)


class KwargsStyleException(NovaStyleException):

    format = 'I am %(who)s'


def add_remote_postfix(ex):
    ex_type = type(ex)
    message = str(ex)
    str_override = lambda self: message
    new_ex_type = type(ex_type.__name__ + "_Remote", (ex_type,),
                       {'__str__': str_override,
                        '__unicode__': str_override})
    new_ex_type.__module__ = '%s_Remote' % ex.__class__.__module__
    try:
        ex.__class__ = new_ex_type
    except TypeError:
        ex.args = (message,) + ex.args[1:]
    return ex


class SerializeRemoteExceptionTestCase(test_utils.BaseTestCase):

    _log_failure = [
        ('log_failure', dict(log_failure=True)),
        ('do_not_log_failure', dict(log_failure=False)),
    ]

    _add_remote = [
        ('add_remote', dict(add_remote=True)),
        ('do_not_add_remote', dict(add_remote=False)),
    ]

    _exception_types = [
        ('bog_standard', dict(cls=Exception,
                              args=['test'],
                              kwargs={},
                              clsname='Exception',
                              modname=EXCEPTIONS_MODULE,
                              msg='test')),
        ('nova_style', dict(cls=NovaStyleException,
                            args=[],
                            kwargs={},
                            clsname='NovaStyleException',
                            modname=__name__,
                            msg='I am Nova')),
        ('nova_style_with_msg', dict(cls=NovaStyleException,
                                     args=['testing'],
                                     kwargs={},
                                     clsname='NovaStyleException',
                                     modname=__name__,
                                     msg='testing')),
        ('kwargs_style', dict(cls=KwargsStyleException,
                              args=[],
                              kwargs={'who': 'Oslo'},
                              clsname='KwargsStyleException',
                              modname=__name__,
                              msg='I am Oslo')),
    ]

    @classmethod
    def generate_scenarios(cls):
        cls.scenarios = testscenarios.multiply_scenarios(cls._log_failure,
                                                         cls._add_remote,
                                                         cls._exception_types)

    def setUp(self):
        super(SerializeRemoteExceptionTestCase, self).setUp()

    def test_serialize_remote_exception(self):
        errors = []

        def stub_error(msg, *a, **kw):
            if (a and len(a) == 1 and isinstance(a[0], dict) and a[0]):
                a = a[0]
            errors.append(str(msg) % a)

        self.stubs.Set(exceptions.LOG, 'error', stub_error)

        try:
            try:
                raise self.cls(*self.args, **self.kwargs)
            except Exception as ex:
                cls_error = ex
                if self.add_remote:
                    ex = add_remote_postfix(ex)
                raise ex
        except Exception:
            exc_info = sys.exc_info()

        serialized = exceptions.serialize_remote_exception(
            exc_info, log_failure=self.log_failure)

        failure = jsonutils.loads(serialized)

        self.assertEqual(failure['class'], self.clsname, failure)
        self.assertEqual(failure['module'], self.modname)
        self.assertEqual(failure['message'], self.msg)
        self.assertEqual(failure['args'], [self.msg])
        self.assertEqual(failure['kwargs'], self.kwargs)

        # Note: _Remote prefix not stripped from tracebacks
        tb = cls_error.__class__.__name__ + ': ' + self.msg
        self.assertIn(tb, ''.join(failure['tb']))

        if self.log_failure:
            self.assertTrue(len(errors) > 0, errors)
        else:
            self.assertEqual(len(errors), 0, errors)


SerializeRemoteExceptionTestCase.generate_scenarios()


class DeserializeRemoteExceptionTestCase(test_utils.BaseTestCase):

    _standard_allowed = [__name__]

    scenarios = [
        ('bog_standard',
         dict(allowed=_standard_allowed,
              clsname='Exception',
              modname=EXCEPTIONS_MODULE,
              cls=Exception,
              args=['test'],
              kwargs={},
              str='test\ntraceback\ntraceback\n',
              remote_name='Exception',
              remote_args=('test\ntraceback\ntraceback\n', ),
              remote_kwargs={})),
        ('nova_style',
         dict(allowed=_standard_allowed,
              clsname='NovaStyleException',
              modname=__name__,
              cls=NovaStyleException,
              args=[],
              kwargs={},
              str='test\ntraceback\ntraceback\n',
              remote_name='NovaStyleException_Remote',
              remote_args=('I am Nova', ),
              remote_kwargs={})),
        ('nova_style_with_msg',
         dict(allowed=_standard_allowed,
              clsname='NovaStyleException',
              modname=__name__,
              cls=NovaStyleException,
              args=['testing'],
              kwargs={},
              str='test\ntraceback\ntraceback\n',
              remote_name='NovaStyleException_Remote',
              remote_args=('testing', ),
              remote_kwargs={})),
        ('kwargs_style',
         dict(allowed=_standard_allowed,
              clsname='KwargsStyleException',
              modname=__name__,
              cls=KwargsStyleException,
              args=[],
              kwargs={'who': 'Oslo'},
              str='test\ntraceback\ntraceback\n',
              remote_name='KwargsStyleException_Remote',
              remote_args=('I am Oslo', ),
              remote_kwargs={})),
        ('not_allowed',
         dict(allowed=[],
              clsname='NovaStyleException',
              modname=__name__,
              cls=messaging.RemoteError,
              args=[],
              kwargs={},
              str=("Remote error: NovaStyleException test\n"
                   "[%r]." % u'traceback\ntraceback\n'),
              msg=("Remote error: NovaStyleException test\n"
                   "[%r]." % u'traceback\ntraceback\n'),
              remote_name='RemoteError',
              remote_args=(),
              remote_kwargs={'exc_type': 'NovaStyleException',
                             'value': 'test',
                             'traceback': 'traceback\ntraceback\n'})),
        ('unknown_module',
         dict(allowed=['notexist'],
              clsname='Exception',
              modname='notexist',
              cls=messaging.RemoteError,
              args=[],
              kwargs={},
              str=("Remote error: Exception test\n"
                   "[%r]." % u'traceback\ntraceback\n'),
              msg=("Remote error: Exception test\n"
                   "[%r]." % u'traceback\ntraceback\n'),
              remote_name='RemoteError',
              remote_args=(),
              remote_kwargs={'exc_type': 'Exception',
                             'value': 'test',
                             'traceback': 'traceback\ntraceback\n'})),
        ('unknown_exception',
         dict(allowed=[],
              clsname='FarcicalError',
              modname=EXCEPTIONS_MODULE,
              cls=messaging.RemoteError,
              args=[],
              kwargs={},
              str=("Remote error: FarcicalError test\n"
                   "[%r]." % u'traceback\ntraceback\n'),
              msg=("Remote error: FarcicalError test\n"
                   "[%r]." % u'traceback\ntraceback\n'),
              remote_name='RemoteError',
              remote_args=(),
              remote_kwargs={'exc_type': 'FarcicalError',
                             'value': 'test',
                             'traceback': 'traceback\ntraceback\n'})),
        ('unknown_kwarg',
         dict(allowed=[],
              clsname='Exception',
              modname=EXCEPTIONS_MODULE,
              cls=messaging.RemoteError,
              args=[],
              kwargs={'foobar': 'blaa'},
              str=("Remote error: Exception test\n"
                   "[%r]." % u'traceback\ntraceback\n'),
              msg=("Remote error: Exception test\n"
                   "[%r]." % u'traceback\ntraceback\n'),
              remote_name='RemoteError',
              remote_args=(),
              remote_kwargs={'exc_type': 'Exception',
                             'value': 'test',
                             'traceback': 'traceback\ntraceback\n'})),
        ('system_exit',
         dict(allowed=[],
              clsname='SystemExit',
              modname=EXCEPTIONS_MODULE,
              cls=messaging.RemoteError,
              args=[],
              kwargs={},
              str=("Remote error: SystemExit test\n"
                   "[%r]." % u'traceback\ntraceback\n'),
              msg=("Remote error: SystemExit test\n"
                   "[%r]." % u'traceback\ntraceback\n'),
              remote_name='RemoteError',
              remote_args=(),
              remote_kwargs={'exc_type': 'SystemExit',
                             'value': 'test',
                             'traceback': 'traceback\ntraceback\n'})),
    ]

    def test_deserialize_remote_exception(self):
        failure = {
            'class': self.clsname,
            'module': self.modname,
            'message': 'test',
            'tb': ['traceback\ntraceback\n'],
            'args': self.args,
            'kwargs': self.kwargs,
        }

        serialized = jsonutils.dumps(failure)

        ex = exceptions.deserialize_remote_exception(serialized, self.allowed)

        self.assertIsInstance(ex, self.cls)
        self.assertEqual(ex.__class__.__name__, self.remote_name)
        self.assertEqual(six.text_type(ex), self.str)
        if hasattr(self, 'msg'):
            self.assertEqual(six.text_type(ex), self.msg)
            self.assertEqual(ex.args, (self.msg,) + self.remote_args)
        else:
            self.assertEqual(ex.args, self.remote_args)
