Code

Ticket #6094: 6094.diff

File 6094.diff, 19.9 KB (added by gwilson, 6 years ago)
Line 
1=== added directory 'tests/regressiontests/handler_exceptions'
2=== added file 'tests/regressiontests/handler_exceptions/__init__.py'
3=== added file 'tests/regressiontests/handler_exceptions/models.py'
4--- tests/regressiontests/handler_exceptions/models.py  1970-01-01 00:00:00 +0000
5+++ tests/regressiontests/handler_exceptions/models.py  2007-12-02 19:40:43 +0000
6@@ -0,0 +1,1 @@
7+# models file for tests to run.
8
9=== added file 'tests/regressiontests/handler_exceptions/tests.py'
10--- tests/regressiontests/handler_exceptions/tests.py   1970-01-01 00:00:00 +0000
11+++ tests/regressiontests/handler_exceptions/tests.py   2007-12-02 21:25:51 +0000
12@@ -0,0 +1,149 @@
13+from django.test import TestCase
14+from django.test.client import Client, ClientHandler
15+
16+class GoodMiddleware(object):
17+    def process_request(self, *args, **kwargs):
18+        pass
19+    def process_view(self, *args, **kwargs):
20+        pass
21+    def process_exception(self, *args, **kwargs):
22+        pass
23+    def process_response(self, request, response):
24+        return response
25+
26+
27+class MiddlewareException(Exception): pass
28+class RequestMiddlewareException(MiddlewareException): pass
29+class ViewMiddlewareException(MiddlewareException): pass
30+class ExceptionMiddlewareException(MiddlewareException): pass
31+class ResponseMiddlewareException(MiddlewareException): pass
32+
33+
34+class BadRequestMiddleware(object):
35+    def process_request(self, *args, **kwargs):
36+        raise RequestMiddlewareException
37+
38+class BadViewMiddleware(object):
39+    def process_view(self, *args, **kwargs):
40+        raise ViewMiddlewareException
41+
42+class BadExceptionMiddleware(object):
43+    def process_exception(self, *args, **kwargs):
44+        raise ExceptionMiddlewareException
45+
46+class BadResponseMiddleware(object):
47+    def process_response(self, *args, **kwargs):
48+        raise ResponseMiddlewareException
49+
50+
51+class MiddlewareResettingClientHandler(ClientHandler):
52+    """
53+    A handler that clears loaded middleware after each request.
54+    """
55+    def __call__(self, *args, **kwargs):
56+        response = super(MiddlewareResettingClientHandler, self).__call__(*args, **kwargs)
57+        # ClientHandler's __call__ method will load middleware if
58+        # self._request_middleware is None.  We set it to None here so that
59+        # middleware will be reloaded next request.
60+        self._request_middleware = None
61+        return response
62+
63+
64+class HandlerClient(Client):
65+    """
66+    A custom Client that doesn't reraise request-handling exceptions and uses
67+    the MiddlewareResettingClientHandler.
68+    """
69+    def __init__(self, *args, **kwargs):
70+        super(HandlerClient, self).__init__(*args, **kwargs)
71+        self.reraise_exceptions = False
72+        self.handler = MiddlewareResettingClientHandler()
73+
74+
75+class HandlerExceptionTests(TestCase):
76+    def setUp(self):
77+        self.client = HandlerClient()
78+        self.default_middleware = None
79+        # Combinations that will raise errors.
80+        self.bad_combinations = (
81+            # Middleware, request path
82+            ('BadRequestMiddleware', '/good_view/', RequestMiddlewareException),
83+            ('BadViewMiddleware', '/good_view/', ViewMiddlewareException),
84+            ('BadResponseMiddleware', '/good_view/', ResponseMiddlewareException),
85+            ('BadRequestMiddleware', '/bad_view/', RequestMiddlewareException),
86+            ('BadViewMiddleware', '/bad_view/', ViewMiddlewareException),
87+            ('BadExceptionMiddleware', '/bad_view/', ExceptionMiddlewareException),
88+            ('BadResponseMiddleware', '/bad_view/', ResponseMiddlewareException),
89+        )
90+        # Combinations that will not raise errors.
91+        self.good_combinations = (
92+            ('GoodMiddleware', '/good_view/'),
93+            # Exception middleware doesn't get run if the view doesn't raise
94+            # an exception.
95+            ('BadExceptionMiddleware', '/good_view/'),
96+        )
97+
98+    def setup_middleware(self, middleware):
99+        """
100+        Append middleware to list of installed middleware.
101+        """
102+        from django.conf import settings
103+        # If we haven't saved the default middleware, do so now.
104+        if self.default_middleware is None:
105+            self.default_middleware = settings.MIDDLEWARE_CLASSES
106+        middleware_path = 'regressiontests.handler_exceptions.tests.' + middleware
107+        settings.MIDDLEWARE_CLASSES += (middleware_path,)
108+
109+    def reset_middleware(self):
110+        """
111+        Restores middleware to the default installed middleware.
112+        """
113+        from django.conf import settings
114+        settings.MIDDLEWARE_CLASSES = self.default_middleware
115+
116+    def test_exception_handling(self):
117+        """
118+        Tests that exceptions raised in middleware and views are properly
119+        caught and render error templates.
120+        """
121+        # Just for good measure, check that the good combination doesn't raise
122+        # an error.
123+        for middleware, path in self.good_combinations:
124+            self.setup_middleware(middleware)
125+            response = self.client.get('/handler_exceptions' + path)
126+            self.assertContains(response, 'good view')
127+            self.reset_middleware()
128+        # Now test that all the bad combinations render the 500 template.
129+        for middleware, path, exception in self.bad_combinations:
130+            self.setup_middleware(middleware)
131+            response = self.client.get('/handler_exceptions' + path)
132+            self.assertContains(response, '500 Error', status_code=500)
133+            self.assertTemplateUsed(response, '500.html')
134+            self.reset_middleware()
135+
136+    def test_exception_raising(self):
137+        """
138+        Tests that the correct exceptions bubble up from middleware with errors.
139+        """
140+        # Turn on client's reraising of exceptions for this test.
141+        self.client.reraise_exceptions = True
142+        for middleware, path, exception in self.bad_combinations:
143+            self.setup_middleware(middleware)
144+            fail = False
145+            try:
146+                self.client.get('/handler_exceptions' + path)
147+                # If above didn't raise an exception, then test failed.
148+                fail = True
149+            except exception:
150+                # We got the exception we were looking for, test passes.
151+                pass
152+            except Exception:
153+                # We got an exception that we wore not expecting.
154+                from sys import exc_info
155+                unexpected_exception = exc_info()[0]
156+                self.fail("Expected %s to be raised, but got %s"
157+                          % (exception, unexpected_exception))
158+            if fail:
159+                self.fail("Expected %s to be raised.  Middleware: %s, Path: %s"
160+                          % (exception, middleware, path))
161+            self.reset_middleware()
162
163=== added file 'tests/regressiontests/handler_exceptions/urls.py'
164--- tests/regressiontests/handler_exceptions/urls.py    1970-01-01 00:00:00 +0000
165+++ tests/regressiontests/handler_exceptions/urls.py    2007-12-02 17:17:21 +0000
166@@ -0,0 +1,8 @@
167+from django.conf.urls.defaults import *
168+
169+import views
170+
171+urlpatterns = patterns('',
172+    (r'^good_view/$', views.good_view),
173+    (r'^bad_view/$', views.bad_view),
174+)
175
176=== added file 'tests/regressiontests/handler_exceptions/views.py'
177--- tests/regressiontests/handler_exceptions/views.py   1970-01-01 00:00:00 +0000
178+++ tests/regressiontests/handler_exceptions/views.py   2007-12-02 18:59:43 +0000
179@@ -0,0 +1,7 @@
180+from django.http import HttpResponse
181+
182+def good_view(request):
183+    return HttpResponse("good view")
184+
185+def bad_view(request):
186+    raise Exception("View raised exception")
187
188=== modified file 'django/core/handlers/base.py'
189--- django/core/handlers/base.py        2007-12-02 02:22:19 +0000
190+++ django/core/handlers/base.py        2007-12-02 19:07:21 +0000
191@@ -53,58 +53,110 @@
192             if hasattr(mw_instance, 'process_exception'):
193                 self._exception_middleware.insert(0, mw_instance.process_exception)
194 
195-    def get_response(self, request):
196-        "Returns an HttpResponse object for the given HttpRequest"
197-        from django.core import exceptions, urlresolvers
198-        from django.core.mail import mail_admins
199+    def handle_request(self, request):
200+        dispatcher.send(signal=signals.request_started)
201+        try:
202+            try:
203+                request = self.request_class(request)
204+            except UnicodeDecodeError:
205+                response = http.HttpResponseBadRequest()
206+            else:
207+                # See if the request middleware returns a response.
208+                response = self.get_response(self.apply_request_middleware,
209+                                             request)
210+                # If not, then call the view.
211+                if not response:
212+                    response = self.get_response(self.run_view_processing,
213+                                                 request)
214+            # All responses go through the response middleware.
215+            response = self.get_response(self.apply_response_middleware,
216+                                         request, response)
217+            response = self.apply_response_fixes(request, response)
218+            return response
219+        finally:
220+            dispatcher.send(signal=signals.request_finished)
221+
222+    def resolver(self, request):
223+        """
224+        Returns a RegexURLResolver for the request's urlconf.
225+
226+        If the request does not have a urlconf object, then the default of
227+        settings.ROOT_URLCONF is used.
228+        """
229+        from django.core.urlresolvers import RegexURLResolver
230         from django.conf import settings
231+        urlconf = getattr(request, "urlconf", settings.ROOT_URLCONF)
232+        return RegexURLResolver(r'^/', urlconf)
233 
234-        # Apply request middleware
235+    def apply_request_middleware(self, request):
236+        """
237+        Applies request middleware one at a time until a response is returned.
238+        If none of request middleware returns a response, then return None.
239+        """
240         for middleware_method in self._request_middleware:
241             response = middleware_method(request)
242             if response:
243                 return response
244 
245-        # Get urlconf from request object, if available.  Otherwise use default.
246-        urlconf = getattr(request, "urlconf", settings.ROOT_URLCONF)
247-
248-        resolver = urlresolvers.RegexURLResolver(r'^/', urlconf)
249+    def run_view_processing(self, request):
250+        """
251+        Apply view middleware and call view.
252+        """
253+        callback, callback_args, callback_kwargs = self.resolver(request).resolve(request.path)
254+
255+        # Apply view middleware
256+        for middleware_method in self._view_middleware:
257+            response = middleware_method(request, callback, callback_args, callback_kwargs)
258+            if response:
259+                return response
260+
261         try:
262-            callback, callback_args, callback_kwargs = resolver.resolve(request.path)
263-
264-            # Apply view middleware
265-            for middleware_method in self._view_middleware:
266-                response = middleware_method(request, callback, callback_args, callback_kwargs)
267+            response = callback(request, *callback_args, **callback_kwargs)
268+        except Exception, e:
269+            # If the view raised an exception, run it through exception
270+            # middleware, and if the exception middleware returns a
271+            # response, use that. Otherwise, reraise the exception.
272+            for middleware_method in self._exception_middleware:
273+                response = middleware_method(request, e)
274                 if response:
275                     return response
276+            raise
277 
278+        # Complain if the view returned None (a common error).
279+        if response is None:
280             try:
281-                response = callback(request, *callback_args, **callback_kwargs)
282-            except Exception, e:
283-                # If the view raised an exception, run it through exception
284-                # middleware, and if the exception middleware returns a
285-                # response, use that. Otherwise, reraise the exception.
286-                for middleware_method in self._exception_middleware:
287-                    response = middleware_method(request, e)
288-                    if response:
289-                        return response
290-                raise
291-
292-            # Complain if the view returned None (a common error).
293+                view_name = callback.func_name # If it's a function
294+            except AttributeError:
295+                view_name = callback.__class__.__name__ + '.__call__' # If it's a class
296+            raise ValueError, "The view %s.%s didn't return an HttpResponse object." % (callback.__module__, view_name)
297+
298+        return response
299+
300+    def apply_response_middleware(self, request, response):
301+        """
302+        Applies all response middleware to the response.
303+        """
304+        for middleware_method in self._response_middleware:
305+            response = middleware_method(request, response)
306+        return response
307+
308+    def get_response(self, method, request, response=None):
309+        "Returns an HttpResponse object for the given HttpRequest"
310+        from django.core import exceptions
311+        from django.core.mail import mail_admins
312+        from django.conf import settings
313+
314+        try:
315             if response is None:
316-                try:
317-                    view_name = callback.func_name # If it's a function
318-                except AttributeError:
319-                    view_name = callback.__class__.__name__ + '.__call__' # If it's a class
320-                raise ValueError, "The view %s.%s didn't return an HttpResponse object." % (callback.__module__, view_name)
321-
322-            return response
323+                return method(request)
324+            else:
325+                return method(request, response)
326         except http.Http404, e:
327             if settings.DEBUG:
328                 from django.views import debug
329                 return debug.technical_404_response(request, e)
330             else:
331-                callback, param_dict = resolver.resolve404()
332+                callback, param_dict = self.resolver(request).resolve404()
333                 return callback(request, **param_dict)
334         except exceptions.PermissionDenied:
335             return http.HttpResponseForbidden('<h1>Permission denied</h1>')
336@@ -127,7 +179,7 @@
337                 message = "%s\n\n%s" % (self._get_traceback(exc_info), request_repr)
338                 mail_admins(subject, message, fail_silently=True)
339                 # Return an HttpResponse that displays a friendly error message.
340-                callback, param_dict = resolver.resolve500()
341+                callback, param_dict = self.resolver(request).resolve500()
342                 return callback(request, **param_dict)
343 
344     def _get_traceback(self, exc_info=None):
345
346=== modified file 'django/core/handlers/modpython.py'
347--- django/core/handlers/modpython.py   2007-12-02 02:22:19 +0000
348+++ django/core/handlers/modpython.py   2007-12-02 07:03:32 +0000
349@@ -2,9 +2,7 @@
350 from pprint import pformat
351 
352 from django import http
353-from django.core import signals
354 from django.core.handlers.base import BaseHandler
355-from django.dispatch import dispatcher
356 from django.utils import datastructures
357 from django.utils.encoding import force_unicode
358 
359@@ -151,21 +149,7 @@
360         if self._request_middleware is None:
361             self.load_middleware()
362 
363-        dispatcher.send(signal=signals.request_started)
364-        try:
365-            try:
366-                request = self.request_class(req)
367-            except UnicodeDecodeError:
368-                response = http.HttpResponseBadRequest()
369-            else:
370-                response = self.get_response(request)
371-
372-                # Apply response middleware
373-                for middleware_method in self._response_middleware:
374-                    response = middleware_method(request, response)
375-                response = self.apply_response_fixes(request, response)
376-        finally:
377-            dispatcher.send(signal=signals.request_finished)
378+        response = self.handle_request(req)
379 
380         # Convert our custom HttpResponse object back into the mod_python req.
381         req.content_type = response['Content-Type']
382
383=== modified file 'django/core/handlers/wsgi.py'
384--- django/core/handlers/wsgi.py        2007-12-02 02:22:19 +0000
385+++ django/core/handlers/wsgi.py        2007-12-02 07:23:13 +0000
386@@ -6,9 +6,7 @@
387     from StringIO import StringIO
388 
389 from django import http
390-from django.core import signals
391 from django.core.handlers.base import BaseHandler
392-from django.dispatch import dispatcher
393 from django.utils import datastructures
394 from django.utils.encoding import force_unicode
395 
396@@ -195,21 +193,7 @@
397                 self.load_middleware()
398             self.initLock.release()
399 
400-        dispatcher.send(signal=signals.request_started)
401-        try:
402-            try:
403-                request = self.request_class(environ)
404-            except UnicodeDecodeError:
405-                response = http.HttpResponseBadRequest()
406-            else:
407-                response = self.get_response(request)
408-
409-                # Apply response middleware
410-                for middleware_method in self._response_middleware:
411-                    response = middleware_method(request, response)
412-                response = self.apply_response_fixes(request, response)
413-        finally:
414-            dispatcher.send(signal=signals.request_finished)
415+        response = self.handle_request(environ)
416 
417         try:
418             status_text = STATUS_CODE_TEXT[response.status_code]
419
420=== modified file 'django/test/client.py'
421--- django/test/client.py       2007-11-11 03:55:44 +0000
422+++ django/test/client.py       2007-12-02 20:42:25 +0000
423@@ -25,28 +25,17 @@
424     Uses the WSGI interface to compose requests, but returns
425     the raw HttpResponse object
426     """
427+    request_class = WSGIRequest
428+
429     def __call__(self, environ):
430         from django.conf import settings
431-        from django.core import signals
432 
433         # Set up middleware if needed. We couldn't do this earlier, because
434         # settings weren't available.
435         if self._request_middleware is None:
436             self.load_middleware()
437 
438-        dispatcher.send(signal=signals.request_started)
439-        try:
440-            request = WSGIRequest(environ)
441-            response = self.get_response(request)
442-
443-            # Apply response middleware
444-            for middleware_method in self._response_middleware:
445-                response = middleware_method(request, response)
446-            response = self.apply_response_fixes(request, response)
447-        finally:
448-            dispatcher.send(signal=signals.request_finished)
449-
450-        return response
451+        return self.handle_request(environ)
452 
453 def store_rendered_templates(store, signal, sender, template, context):
454     "A utility function for storing templates and contexts that are rendered"
455@@ -96,7 +85,7 @@
456     ])
457     return '\r\n'.join(lines)
458 
459-class Client:
460+class Client(object):
461     """
462     A class that can act as a client for testing purposes.
463 
464@@ -119,6 +108,7 @@
465         self.defaults = defaults
466         self.cookies = SimpleCookie()
467         self.exc_info = None
468+        self.reraise_exceptions = True
469 
470     def store_exc_info(self, *args, **kwargs):
471         """
472@@ -180,7 +170,7 @@
473                 raise
474 
475         # Look for a signalled exception and reraise it
476-        if self.exc_info:
477+        if self.exc_info and self.reraise_exceptions:
478             raise self.exc_info[1], None, self.exc_info[2]
479 
480         # Save the client and request that stimulated the response
481@@ -262,7 +252,7 @@
482             self.cookies[settings.SESSION_COOKIE_NAME]['expires'] = None
483 
484             # Save the session values
485-            request.session.save()   
486+            request.session.save()
487 
488             return True
489         else:
490
491=== modified file 'tests/urls.py'
492--- tests/urls.py       2007-09-17 14:48:33 +0000
493+++ tests/urls.py       2007-12-02 17:31:46 +0000
494@@ -14,4 +14,6 @@
495     
496     # django built-in views
497     (r'^views/', include('regressiontests.views.urls')),
498+   
499+    (r'^handler_exceptions/', include('regressiontests.handler_exceptions.urls')),
500 )
501