#31944: Use addCleanup() to register TestContextDecorator cleanups
-------------------------------------+-------------------------------------
     Reporter:  François Freitag     |                    Owner:  nobody
         Type:                       |                   Status:  new
  Cleanup/optimization               |
    Component:  Testing framework    |                  Version:  master
     Severity:  Normal               |               Resolution:
     Keywords:                       |             Triage Stage:
                                     |  Unreviewed
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------

Old description:

> `unittest.TestCase` offers an
> [https://docs.python.org/3/library/unittest.html#unittest.TestCase.addCleanup
> addCleanup()] function that is guaranteed to run even if
> `unittest.TestCase.setUp()` fails.
>
> In #29024, the cleanup was implemented by
> [https://github.com/django/django/blob/0b0658111cba538b91072b9a133fd5545f3f46d1/django/test/utils.py#L357-L361
> catching Exception, calling TestContextDecorator.disable() and re-
> raising]. Using `addCleanup()` allows a small simplification. More
> importantly, it prevents an ordering issue with the cleanups that can
> cause tests to bleed when subclasses of Django `TestCase` relying of
> `addCleanup()` have their cleanups occurring after the
> `TestContextDecorator.disable()`. To illustrate:
>
> {{{#!python
> class ChangeCSRFCookieName(TestContextDecorator):
>     def __init__(self, *args, **kwargs):
>         super().__init__(*args, **kwargs)
>         self.stack = contextlib.ExitStack()
>
>     def enable(self):
>         cm =
> override_settings(CSRF_COOKIE_NAME="cross_site_request_forgery_token")
>         self.stack.enter_context(cm)
>
>     def disable(self):
>         self.stack.close()
>

> class ChangeSettingsTestCase(SimpleTestCase):
>     def setUp(self):
>         super().setUp()
>         settings_cm = self.settings(CACHES={})
>         settings_cm.enable()
>         self.addCleanup(settings_cm.disable)
>

> @ChangeCSRFCookieName()
> class ChangeCSRFCookieNameTest(ChangeSettingsTestCase):
>     def test_initial(self):
>         pass
>

> class LeakTest(SimpleTestCase):
>     def test_csrf_cookie_name_has_leaked(self):
>         # Should use the default value.
>         self.assertEqual("csrftoken", settings.CSRF_COOKIE_NAME)
>

> # Fails with:
> #======================================================================
> #FAIL: test_csrf_cookie_name_has_leaked (test_utils.tests.LeakTest)
> #----------------------------------------------------------------------
> #Traceback (most recent call last):
> #  File "/home/freitafr/dev/django-side/tests/test_utils/tests.py", line
> 1514, in test_csrf_cookie_name_has_leaked
> #    self.assertEqual("csrftoken", settings.CSRF_COOKIE_NAME)
> #AssertionError: 'csrftoken' != 'cross_site_request_forgery_token'
> #- csrftoken
> #+ cross_site_request_forgery_token
> }}}
>
> The cleanups from `addCleanup()` are scheduled to happen in reverse order
> to the order they are added (LIFO). That should make sure each cleanup is
> executed from the innermost to the outermost.
>
> I am happy to submit a patch for the fix, but I am concerned the change
> may be backward incompatible. Perhaps people relied on this ordering, and
> “fixing” the ordering will trigger test bleeding in their test suite. Is
> `TestContextDecorator` considered private enough to be changed with a
> mention in the release note?

New description:

 `unittest.TestCase` offers an
 [https://docs.python.org/3/library/unittest.html#unittest.TestCase.addCleanup
 addCleanup()] function that is guaranteed to run even if
 `unittest.TestCase.setUp()` fails.

 In #29024, the cleanup was implemented by
 
[https://github.com/django/django/blob/0b0658111cba538b91072b9a133fd5545f3f46d1/django/test/utils.py#L357-L361
 catching Exception, calling TestContextDecorator.disable() and re-
 raising]. Using `addCleanup()` allows a small simplification. More
 importantly, it prevents an ordering issue with the cleanups that can
 cause tests to bleed when subclasses of Django `TestCase` relying of
 `addCleanup()` have their cleanups occurring after the
 `TestContextDecorator.disable()`. To illustrate:

 {{{#!python
 class ChangeCSRFCookieName(TestContextDecorator):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.stack = contextlib.ExitStack()

     def enable(self):
         # 1. Before setUp, TestContextDecorator runs enable().
         cm =
 override_settings(CSRF_COOKIE_NAME="cross_site_request_forgery_token")
         self.stack.enter_context(cm)

     def disable(self):
         # 3. After tearDown, TestContextDecorator runs disable(), which
 restores settings captured in 1.
         # This should be step 4.
         self.stack.close()


 class ChangeSettingsTestCase(SimpleTestCase):
     def setUp(self):
         super().setUp()
         # 2. Capture settings after they’ve been changed in 1.
         settings_cm = self.settings(CACHES={})
         settings_cm.enable()
         self.addCleanup(settings_cm.disable)  # 4. Restore settings
 captured in 2. Should be executed at step 3.


 @ChangeCSRFCookieName()
 class ChangeCSRFCookieNameTest(ChangeSettingsTestCase):
     def test_initial(self):
         pass


 class LeakTest(SimpleTestCase):
     def test_csrf_cookie_name_has_leaked(self):
         # Should use the default value.
         self.assertEqual("csrftoken", settings.CSRF_COOKIE_NAME)


 # Fails with:
 #======================================================================
 #FAIL: test_csrf_cookie_name_has_leaked (test_utils.tests.LeakTest)
 #----------------------------------------------------------------------
 #Traceback (most recent call last):
 #  File "/home/freitafr/dev/django-side/tests/test_utils/tests.py", line
 1514, in test_csrf_cookie_name_has_leaked
 #    self.assertEqual("csrftoken", settings.CSRF_COOKIE_NAME)
 #AssertionError: 'csrftoken' != 'cross_site_request_forgery_token'
 #- csrftoken
 #+ cross_site_request_forgery_token
 }}}

 The cleanups from `addCleanup()` are scheduled to happen in reverse order
 to the order they are added (LIFO). That should make sure each cleanup is
 executed from the innermost to the outermost.

 I am happy to submit a patch for the fix, but I am concerned the change
 may be backward incompatible. Perhaps people relied on this ordering, and
 “fixing” the ordering will trigger test bleeding in their test suite. Is
 `TestContextDecorator` considered private enough to be changed with a
 mention in the release note?

--

Comment (by François Freitag):

 Clarified ordering issue with comments in the example code.

-- 
Ticket URL: <https://code.djangoproject.com/ticket/31944#comment:1>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/073.611ba0d5b1c5280873b228875d7038ef%40djangoproject.com.

Reply via email to