Fixed #29467 -- Made override_settings handle errors in setting_changed signal receivers.

This commit is contained in:
Sławek Ehlert 2018-06-19 23:10:55 +02:00 committed by Tim Graham
parent cebbcaa1ba
commit c6238bf02b
2 changed files with 126 additions and 4 deletions

View File

@ -382,6 +382,8 @@ class override_settings(TestContextDecorator):
with the ``with`` statement. In either event, entering/exiting are called
before and after, respectively, the function/block is executed.
"""
enable_exception = None
def __init__(self, **kwargs):
self.options = kwargs
super().__init__()
@ -401,18 +403,35 @@ class override_settings(TestContextDecorator):
self.wrapped = settings._wrapped
settings._wrapped = override
for key, new_value in self.options.items():
setting_changed.send(sender=settings._wrapped.__class__,
setting=key, value=new_value, enter=True)
try:
setting_changed.send(
sender=settings._wrapped.__class__,
setting=key, value=new_value, enter=True,
)
except Exception as exc:
self.enable_exception = exc
self.disable()
def disable(self):
if 'INSTALLED_APPS' in self.options:
apps.unset_installed_apps()
settings._wrapped = self.wrapped
del self.wrapped
responses = []
for key in self.options:
new_value = getattr(settings, key, None)
setting_changed.send(sender=settings._wrapped.__class__,
setting=key, value=new_value, enter=False)
responses_for_setting = setting_changed.send_robust(
sender=settings._wrapped.__class__,
setting=key, value=new_value, enter=False,
)
responses.extend(responses_for_setting)
if self.enable_exception is not None:
exc = self.enable_exception
self.enable_exception = None
raise exc
for _, response in responses:
if isinstance(response, Exception):
raise response
def save_options(self, test_func):
if test_func._overridden_settings is None:

View File

@ -441,3 +441,106 @@ class TestListSettings(unittest.TestCase):
finally:
del sys.modules['fake_settings_module']
delattr(settings_module, setting)
class SettingChangeEnterException(Exception):
pass
class SettingChangeExitException(Exception):
pass
class OverrideSettingsIsolationOnExceptionTests(SimpleTestCase):
"""
The override_settings context manager restore settings if one of the
receivers of "setting_changed" signal fails. Check the three cases of
receiver failure detailed in receiver(). In each case, ALL receivers are
called when exiting the context manager.
"""
def setUp(self):
signals.setting_changed.connect(self.receiver)
self.addCleanup(signals.setting_changed.disconnect, self.receiver)
# Create a spy that's connected to the `setting_changed` signal and
# executed AFTER `self.receiver`.
self.spy_receiver = mock.Mock()
signals.setting_changed.connect(self.spy_receiver)
self.addCleanup(signals.setting_changed.disconnect, self.spy_receiver)
def receiver(self, **kwargs):
"""
A receiver that fails while certain settings are being changed.
- SETTING_BOTH raises an error while receiving the signal
on both entering and exiting the context manager.
- SETTING_ENTER raises an error only on enter.
- SETTING_EXIT raises an error only on exit.
"""
setting = kwargs['setting']
enter = kwargs['enter']
if setting in ('SETTING_BOTH', 'SETTING_ENTER') and enter:
raise SettingChangeEnterException
if setting in ('SETTING_BOTH', 'SETTING_EXIT') and not enter:
raise SettingChangeExitException
def check_settings(self):
"""Assert that settings for these tests aren't present."""
self.assertFalse(hasattr(settings, 'SETTING_BOTH'))
self.assertFalse(hasattr(settings, 'SETTING_ENTER'))
self.assertFalse(hasattr(settings, 'SETTING_EXIT'))
self.assertFalse(hasattr(settings, 'SETTING_PASS'))
def check_spy_receiver_exit_calls(self, call_count):
"""
Assert that `self.spy_receiver` was called exactly `call_count` times
with the ``enter=False`` keyword argument.
"""
kwargs_with_exit = [
kwargs for args, kwargs in self.spy_receiver.call_args_list
if ('enter', False) in kwargs.items()
]
self.assertEqual(len(kwargs_with_exit), call_count)
def test_override_settings_both(self):
"""Receiver fails on both enter and exit."""
with self.assertRaises(SettingChangeEnterException):
with override_settings(SETTING_PASS='BOTH', SETTING_BOTH='BOTH'):
pass
self.check_settings()
# Two settings were touched, so expect two calls of `spy_receiver`.
self.check_spy_receiver_exit_calls(call_count=2)
def test_override_settings_enter(self):
"""Receiver fails on enter only."""
with self.assertRaises(SettingChangeEnterException):
with override_settings(SETTING_PASS='ENTER', SETTING_ENTER='ENTER'):
pass
self.check_settings()
# Two settings were touched, so expect two calls of `spy_receiver`.
self.check_spy_receiver_exit_calls(call_count=2)
def test_override_settings_exit(self):
"""Receiver fails on exit only."""
with self.assertRaises(SettingChangeExitException):
with override_settings(SETTING_PASS='EXIT', SETTING_EXIT='EXIT'):
pass
self.check_settings()
# Two settings were touched, so expect two calls of `spy_receiver`.
self.check_spy_receiver_exit_calls(call_count=2)
def test_override_settings_reusable_on_enter(self):
"""
Error is raised correctly when reusing the same override_settings
instance.
"""
@override_settings(SETTING_ENTER='ENTER')
def decorated_function():
pass
with self.assertRaises(SettingChangeEnterException):
decorated_function()
signals.setting_changed.disconnect(self.receiver)
# This call shouldn't raise any errors.
decorated_function()