From c6238bf02b7ca5cf0f31f893a951f257dc557600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Ehlert?= Date: Tue, 19 Jun 2018 23:10:55 +0200 Subject: [PATCH] Fixed #29467 -- Made override_settings handle errors in setting_changed signal receivers. --- django/test/utils.py | 27 +++++++-- tests/settings_tests/tests.py | 103 ++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 4 deletions(-) diff --git a/django/test/utils.py b/django/test/utils.py index 0e04e54964..825c6b6d3b 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -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: diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py index 383345494d..bfd580b600 100644 --- a/tests/settings_tests/tests.py +++ b/tests/settings_tests/tests.py @@ -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()