Merge lp:~lifeless/python-oops-wsgi/foru1 into lp:python-oops-wsgi

Proposed by Robert Collins
Status: Merged
Merged at revision: 18
Proposed branch: lp:~lifeless/python-oops-wsgi/foru1
Merge into: lp:python-oops-wsgi
Diff against target: 682 lines (+422/-56)
8 files modified
README (+8/-1)
oops_wsgi/__init__.py (+20/-1)
oops_wsgi/hooks.py (+83/-0)
oops_wsgi/middleware.py (+25/-5)
oops_wsgi/tests/__init__.py (+1/-0)
oops_wsgi/tests/test_hooks.py (+96/-0)
oops_wsgi/tests/test_middleware.py (+188/-48)
setup.py (+1/-1)
To merge this branch: bzr merge lp:~lifeless/python-oops-wsgi/foru1
Reviewer Review Type Date Requested Status
Steve Kowalik (community) code Approve
Review via email: mp+72640@code.launchpad.net

Commit message

Fix several usability bugs.

Description of the change

This fixes limitations in python-oops-wsgi preventing Ubuntu one from using it.

To post a comment you must log in.
Revision history for this message
Steve Kowalik (stevenk) wrote :

There is an errant \ on line 284 of the diff.

review: Approve (code)
lp:~lifeless/python-oops-wsgi/foru1 updated
17. By Robert Collins

Remove typo.

18. By Robert Collins

And actually glue in the environment from make_app.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README'
2--- README 2011-08-17 09:43:07 +0000
3+++ README 2011-08-24 01:57:31 +0000
4@@ -53,6 +53,13 @@
5 Note that you will probably want at least one publisher, or your reports will
6 be discarded.
7
8+* Add in wsgi specific hooks to the config::
9+
10+ >>> oops_wsgi.install_hooks(config)
11+
12+This is a convenience function - you are welcome to pick and choose the creation
13+or filter hooks you want from oops_wsgi.hooks.
14+
15 * Create your wsgi app as normal, and then wrap it::
16
17 >>> app = oops_wsgi.make_app(app, config)
18@@ -83,7 +90,7 @@
19 >>> json_template='{"oopsid" : "%(id)s"}'
20 >>> app = oops_wsgi.make_app(app, config, error_template=json_template)
21
22-More coming soon.
23+For more information see pydoc oops_wsgi.
24
25 Installation
26 ============
27
28=== modified file 'oops_wsgi/__init__.py'
29--- oops_wsgi/__init__.py 2011-08-22 07:07:16 +0000
30+++ oops_wsgi/__init__.py 2011-08-24 01:57:31 +0000
31@@ -30,6 +30,13 @@
32 Note that you will probably want at least one publisher, or your reports will
33 be discarded.
34
35+* Add in wsgi specific hooks to the config::
36+
37+ >>> oops_wsgi.install_hooks(config)
38+
39+This is a convenience function - you are welcome to pick and choose the creation
40+or filter hooks you want from oops_wsgi.hooks.
41+
42 * Create your wsgi app as normal, and then wrap it::
43
44 >>> app = oops_wsgi.make_app(app, config)
45@@ -59,6 +66,16 @@
46
47 >>> json_template='{"oopsid" : "%(id)s"}'
48 >>> app = oops_wsgi.make_app(app, config, error_template=json_template)
49+
50+If the wrapped app errors by sending exc_info to start_response, that will be
51+used to create an OOPS report, and the id added to the headers under the
52+X-Oops-Id header. This is also present when an OOPS is triggered by catching an
53+exception in the wrapped app (as long as the body hasn't started).
54+
55+You can request that reports be created when a given status code is used (e.g.
56+to gather stats on the number of 404's occuring without doing log processing).
57+
58+ >>> app = oops_wsgi.make_app(app, config, oops_on_status=['404'])
59 """
60
61
62@@ -73,10 +90,12 @@
63 # established at this point, and setup.py will use a version of next-$(revno).
64 # If the releaselevel is 'final', then the tarball will be major.minor.micro.
65 # Otherwise it is major.minor.micro~$(revno).
66-__version__ = (0, 0, 2, 'beta', 0)
67+__version__ = (0, 0, 3, 'beta', 0)
68
69 __all__ = [
70+ 'install_hooks',
71 'make_app'
72 ]
73
74 from oops_wsgi.middleware import make_app
75+from oops_wsgi.hooks import install_hooks
76
77=== added file 'oops_wsgi/hooks.py'
78--- oops_wsgi/hooks.py 1970-01-01 00:00:00 +0000
79+++ oops_wsgi/hooks.py 2011-08-24 01:57:31 +0000
80@@ -0,0 +1,83 @@
81+# Copyright (c) 2010, 2011, Canonical Ltd
82+#
83+# This program is free software: you can redistribute it and/or modify
84+# it under the terms of the GNU Affero General Public License as published by
85+# the Free Software Foundation, either version 3 of the License, or
86+# (at your option) any later version.
87+#
88+# This program is distributed in the hope that it will be useful,
89+# but WITHOUT ANY WARRANTY; without even the implied warranty of
90+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
91+# GNU Affero General Public License for more details.
92+#
93+# You should have received a copy of the GNU Affero General Public License
94+# along with this program. If not, see <http://www.gnu.org/licenses/>.
95+# GNU Affero General Public License version 3 (see the file LICENSE).
96+
97+"""oops creation and filtering hooks for working with WSGI."""
98+
99+__all__ = [
100+ 'copy_environ',
101+ 'hide_cookie',
102+ 'install_hooks',
103+ ]
104+
105+_wsgi_standard_env_keys = set([
106+ 'REQUEST_METHOD',
107+ 'SCRIPT_NAME',
108+ 'PATH_INFO',
109+ 'QUERY_STRING',
110+ 'CONTENT_TYPE',
111+ 'CONTENT_LENGTH',
112+ 'SERVER_NAME',
113+ 'SERVER_PORT',
114+ 'SERVER_PROTOCOL',
115+ 'wsgi.version',
116+ 'wsgi.url_scheme',
117+ ])
118+
119+
120+def copy_environ(report, context):
121+ """Copy useful variables from the wsgi environment if it is present.
122+
123+ This should be in the context as 'wsgi_environ'.
124+
125+ e.g.
126+ report = config.create(context=dict(wsgi_environ=environ))
127+ """
128+ environ = context.get('wsgi_environ', {})
129+ if 'req_vars' not in report:
130+ report['req_vars'] = []
131+ req_vars = report['req_vars']
132+ for key, value in sorted(environ.items()):
133+ if (key in _wsgi_standard_env_keys or
134+ key.startswith('HTTP_')):
135+ req_vars.append((key, value))
136+
137+
138+def hide_cookie(report, context):
139+ """If there is an HTTP_COOKIE entry in the report, hide its value.
140+
141+ The entry is looked for either as a top level key or in the req_vars list.
142+
143+ The COOKIE header is often used to carry session tokens and thus permits
144+ folk analyzing crash reports to log in as an arbitrary user (e.g. your
145+ sysadmin users).
146+ """
147+ if 'HTTP_COOKIE' in report:
148+ report['HTTP_COOKIE'] = '<hidden>'
149+ if 'req_vars' not in report:
150+ return
151+ new_vars = []
152+ for key, value in report['req_vars']:
153+ if key == 'HTTP_COOKIE':
154+ value = '<hidden>'
155+ new_vars.append((key, value))
156+ report['req_vars'][:] = new_vars
157+
158+
159+def install_hooks(config):
160+ """Install the default wsgi hooks into config."""
161+ config.on_create.extend([copy_environ, hide_cookie])
162+
163+
164
165=== modified file 'oops_wsgi/middleware.py'
166--- oops_wsgi/middleware.py 2011-08-22 06:58:07 +0000
167+++ oops_wsgi/middleware.py 2011-08-24 01:57:31 +0000
168@@ -40,7 +40,7 @@
169
170
171 def make_app(app, config, template=default_error_template,
172- content_type='text/html', error_render=None):
173+ content_type='text/html', error_render=None, oops_on_status=None):
174 """Construct a middleware around app that will forward errors via config.
175
176 Any errors encountered by the app will be forwarded to config and an error
177@@ -64,6 +64,11 @@
178 :param error_render: Optional custom renderer for presenting error reports
179 to clients. Should be a callable taking the report as its only
180 parameter.
181+ :param oops_on_status: Optional list of HTTP status codes that should
182+ generate OOPSes. OOPSes triggered by sniffing these codes will not
183+ interfere with the response being sent. For instance, if you do
184+ not expect any 404's from your application, you might set
185+ oops_on_status=['404'].
186 :return: A WSGI app.
187 """
188 def oops_middleware(environ, start_response):
189@@ -93,14 +98,26 @@
190 # choose to add the OOPS id to the headers (but we can't fiddle
191 # the body as it is being generated by the contained app.
192 report = config.create(
193- dict(exc_info=exc_info, url=construct_url(environ)))
194+ dict(exc_info=exc_info, url=construct_url(environ),
195+ wsgi_environ=environ))
196 ids = config.publish(report)
197 try:
198+ if ids:
199+ headers = list(headers)
200+ headers.append(('X-Oops-Id', str(report['id'])))
201 state['write'] = start_response(status, headers, exc_info)
202 return state['write']
203 finally:
204 del exc_info
205 else:
206+ if oops_on_status:
207+ for sniff_status in oops_on_status:
208+ if status.startswith(sniff_status):
209+ report = config.create(
210+ dict(url=construct_url(environ),
211+ wsgi_environ=environ))
212+ report['HTTP_STATUS'] = status.split(' ')[0]
213+ config.publish(report)
214 state['response'] = (status, headers)
215 return oops_write
216 try:
217@@ -118,7 +135,8 @@
218 except Exception:
219 exc_info = sys.exc_info()
220 report = config.create(
221- dict(exc_info=exc_info, url=construct_url(environ)))
222+ dict(exc_info=exc_info, url=construct_url(environ),
223+ wsgi_environ=environ))
224 ids = config.publish(report)
225 if not ids or 'write' in state:
226 # No OOPS generated, no oops publisher, or we have already
227@@ -126,8 +144,10 @@
228 # replace the content with a clean error, so let the wsgi
229 # server figure it out.
230 raise
231- start_response('500 Internal Server Error',
232- [('Content-Type', content_type)], exc_info=exc_info)
233+ headers = [('Content-Type', content_type)]
234+ headers.append(('X-Oops-Id', str(report['id'])))
235+ start_response(
236+ '500 Internal Server Error', headers, exc_info=exc_info)
237 del exc_info
238 if error_render is not None:
239 yield error_render(report)
240
241=== modified file 'oops_wsgi/tests/__init__.py'
242--- oops_wsgi/tests/__init__.py 2011-08-17 07:27:56 +0000
243+++ oops_wsgi/tests/__init__.py 2011-08-24 01:57:31 +0000
244@@ -21,6 +21,7 @@
245
246 def test_suite():
247 test_mod_names = [
248+ 'hooks',
249 'middleware',
250 ]
251 return TestLoader().loadTestsFromNames(
252
253=== added file 'oops_wsgi/tests/test_hooks.py'
254--- oops_wsgi/tests/test_hooks.py 1970-01-01 00:00:00 +0000
255+++ oops_wsgi/tests/test_hooks.py 2011-08-24 01:57:31 +0000
256@@ -0,0 +1,96 @@
257+# Copyright (c) 2010, 2011, Canonical Ltd
258+#
259+# This program is free software: you can redistribute it and/or modify
260+# it under the terms of the GNU Affero General Public License as published by
261+# the Free Software Foundation, either version 3 of the License, or
262+# (at your option) any later version.
263+#
264+# This program is distributed in the hope that it will be useful,
265+# but WITHOUT ANY WARRANTY; without even the implied warranty of
266+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
267+# GNU Affero General Public License for more details.
268+#
269+# You should have received a copy of the GNU Affero General Public License
270+# along with this program. If not, see <http://www.gnu.org/licenses/>.
271+# GNU Affero General Public License version 3 (see the file LICENSE).
272+
273+"""Tests for the various hooks included in oops-wsgi."""
274+
275+from StringIO import StringIO
276+
277+from oops import Config
278+from testtools import TestCase
279+from testtools.matchers import LessThan
280+
281+from oops_wsgi import install_hooks
282+from oops_wsgi.hooks import (
283+ copy_environ,
284+ hide_cookie,
285+ )
286+
287+
288+class TestInstallHooks(TestCase):
289+
290+ def test_install_hooks_installs_defaults(self):
291+ config = Config()
292+ install_hooks(config)
293+ self.assertIn(hide_cookie, config.on_create)
294+ self.assertIn(copy_environ, config.on_create)
295+ self.assertThat(
296+ config.on_create.index(copy_environ),
297+ LessThan(config.on_create.index(hide_cookie)),
298+ )
299+
300+
301+class TestHooks(TestCase):
302+
303+ def test_hide_cookie_no_cookie(self):
304+ report = {}
305+ hide_cookie(report, {})
306+ self.assertEqual({}, report)
307+
308+ def test_hide_cookie_cookie_present_top_level(self):
309+ report = {'HTTP_COOKIE': 'foo'}
310+ hide_cookie(report, {})
311+ self.assertEqual({'HTTP_COOKIE': '<hidden>'}, report)
312+
313+ def test_hide_cookie_cookie_present_req_vars(self):
314+ report = {'req_vars': [('HTTP_COOKIE', 'foo')]}
315+ hide_cookie(report, {})
316+ self.assertEqual({'req_vars': [('HTTP_COOKIE', '<hidden>')]}, report)
317+
318+ def test_copy_environ_copied_variables(self):
319+ environ = {
320+ 'REQUEST_METHOD': 'GET',
321+ 'SCRIPT_NAME': '',
322+ 'PATH_INFO': '/foo',
323+ 'QUERY_STRING': 'bar=quux',
324+ 'CONTENT_TYPE': 'multipart/x-form-encoding',
325+ 'CONTENT_LENGTH': '12',
326+ 'SERVER_NAME': 'example.com',
327+ 'SERVER_PORT': '80',
328+ 'SERVER_PROTOCOL': 'HTTP/1.0',
329+ 'HTTP_COOKIE': 'zaphod',
330+ 'wsgi.version': (1, 0),
331+ 'wsgi.url_scheme': 'https',
332+ 'wsgi.input': StringIO(),
333+ }
334+ context = dict(wsgi_environ=environ)
335+ report = {}
336+ copy_environ(report, context)
337+ expected_vars = sorted({
338+ 'REQUEST_METHOD': 'GET',
339+ 'SCRIPT_NAME': '',
340+ 'PATH_INFO': '/foo',
341+ 'QUERY_STRING': 'bar=quux',
342+ 'CONTENT_TYPE': 'multipart/x-form-encoding',
343+ 'CONTENT_LENGTH': '12',
344+ 'SERVER_NAME': 'example.com',
345+ 'SERVER_PORT': '80',
346+ 'SERVER_PROTOCOL': 'HTTP/1.0',
347+ 'HTTP_COOKIE': 'zaphod',
348+ 'wsgi.version': (1, 0),
349+ 'wsgi.url_scheme': 'https',
350+ }.items())
351+ expected_report = {'req_vars': expected_vars}
352+ self.assertEqual(expected_report, report)
353
354=== modified file 'oops_wsgi/tests/test_middleware.py'
355--- oops_wsgi/tests/test_middleware.py 2011-08-22 07:40:30 +0000
356+++ oops_wsgi/tests/test_middleware.py 2011-08-24 01:57:31 +0000
357@@ -30,29 +30,73 @@
358 from testtools import TestCase
359 from testtools.matchers import (
360 DocTestMatches,
361+ Equals,
362+ MatchesListwise,
363+ Mismatch,
364+ MismatchesAll,
365 raises,
366 )
367
368 from oops_wsgi import make_app
369
370
371+class MatchesOOPS:
372+ """Matches an OOPS checking some keys and ignoring the rest."""
373+
374+ def __init__(self, checkkeys):
375+ """Create a MatchesOOPS.
376+
377+ :param checkkeys: A dict describing the keys to check. For each key in
378+ the dict the report must either have a matching key with the same value
379+ or a matching key whose value is matched by the matcher given.
380+ e.g. MatchesOOPS(
381+ dict(id=2, req_vars=MatchesSetwise(Equals(("foo", "bar")))))
382+ will check the id is 2, that req_vars is equivalent to [("foo", "bar")]
383+ and will ignore all other keys in the report.
384+ """
385+ self.checkkeys = checkkeys
386+
387+ def match(self, report):
388+ sentinel = object()
389+ mismatches = []
390+ for key, value in self.checkkeys.items():
391+ if key not in report:
392+ mismatches.append(Mismatch("Report has no key '%s'" % key))
393+ if getattr(value, 'match', sentinel) is sentinel:
394+ matcher = Equals(value)
395+ else:
396+ matcher = value
397+ mismatch = matcher.match(report[key])
398+ if mismatch is not None:
399+ mismatches.append(mismatch)
400+ if mismatches:
401+ return MismatchesAll(mismatches)
402+
403+ def __str__(self):
404+ return "MatchesOOPS(%s)" % self.checkkeys
405+
406+
407 class TestMakeApp(TestCase):
408
409 def setUp(self):
410 super(TestMakeApp, self).setUp()
411 self.calls = []
412
413- def wrap_and_run(self, inner, config, failing_write=False,
414- failing_start=False, failing_server_write=False, params=None):
415- if not params:
416- params = {}
417- app = make_app(inner, config, **params)
418+ def make_environ(self):
419 environ = {}
420 # Shove enough stuff in to let url reconstruction work:
421 environ['wsgi.url_scheme'] = 'http'
422 environ['HTTP_HOST'] = 'example.com'
423 environ['PATH_INFO'] = '/demo'
424 environ['QUERY_STRING'] = 'param=value'
425+ return environ
426+
427+ def wrap_and_run(self, inner, config, failing_write=False,
428+ failing_start=False, failing_server_write=False, params=None):
429+ if not params:
430+ params = {}
431+ app = make_app(inner, config, **params)
432+ environ = self.make_environ()
433 def start_response(status, headers, exc_info=None):
434 if exc_info:
435 # If an exception is raised after we have written (via the app
436@@ -177,17 +221,39 @@
437 raises(ValueError('boom, yo')))
438 self.assertEqual([], self.calls)
439
440- def config_for_oopsing(self):
441+ def config_for_oopsing(self, capture_create=False):
442 config = Config()
443- # datestamps are hell on tests
444- config.on_create.remove(createhooks.attach_date)
445- # so are file paths
446- def remove_tb_text(report, context):
447- report.pop('tb_text')
448- config.on_create.append(remove_tb_text)
449 config.publishers.append(self.publish_to_calls)
450+ if capture_create:
451+ config.on_create.append(self.context_to_calls)
452 return config
453
454+ def context_to_calls(self, report, context):
455+ self.calls.append(context)
456+
457+ def test_oops_start_reponse_adds_x_oops_id_header(self):
458+ # When the oops middleware generates the error page itself, it includes
459+ # an x-oops-id header (vs adding one when we decorate a start_response
460+ # including exc_info from the wrapped app.
461+ def inner(environ, start_response):
462+ raise ValueError('booyah')
463+ config = self.config_for_oopsing()
464+ self.wrap_and_run(inner, config)
465+ headers = [
466+ ('Content-Type', 'text/html'),
467+ ('X-Oops-Id', '1')
468+ ]
469+ expected_start_response = \
470+ ('start error', '500 Internal Server Error', headers, ValueError,
471+ ('booyah',))
472+ self.assertThat(self.calls, MatchesListwise([
473+ # The oops is logged:
474+ MatchesOOPS({'value': 'booyah'}),
475+ # And the containing start_response was called with our custom
476+ # headers.
477+ Equals(expected_start_response),
478+ ]))
479+
480 def test_start_response_500_if_fails_after_start_before_body(self):
481 # oops middle ware needs to buffer the start_response call in case the
482 # wrapped app blows up before it starts streaming data - otherwise the
483@@ -199,16 +265,16 @@
484 yield ''
485 config = self.config_for_oopsing()
486 iterated = self.wrap_and_run(inner, config)
487- self.assertEqual([
488+ self.assertThat(self.calls, MatchesListwise([
489 # First the oops is generated
490- {'id': 1,
491- 'type': 'ValueError',
492- 'url': 'http://example.com/demo?param=value',
493- 'value': u'boom, yo'},
494+ MatchesOOPS({'value': 'boom, yo'}),
495 # Then the middleware responses
496- ('start error', '500 Internal Server Error',
497- [('Content-Type', 'text/html')], ValueError, ('boom, yo',)),
498- ], self.calls)
499+ Equals((
500+ 'start error',
501+ '500 Internal Server Error',
502+ [('Content-Type', 'text/html'), ('X-Oops-Id', '1')],
503+ ValueError, ('boom, yo',))),
504+ ]))
505 self.assertThat(iterated, DocTestMatches(dedent("""\
506 <html>
507 <head><title>Oops! - ...</title></head>
508@@ -228,16 +294,16 @@
509 iterated = self.wrap_and_run(inner, config,
510 params=dict(content_type='text/json',
511 template='{"oopsid" : "%(id)s"}'))
512- self.assertEqual([
513+ self.assertThat(self.calls, MatchesListwise([
514 # First the oops is generated
515- {'id': 1,
516- 'type': 'ValueError',
517- 'url': 'http://example.com/demo?param=value',
518- 'value': u'boom, yo'},
519+ MatchesOOPS({'value': 'boom, yo'}),
520 # Then the middleware responses
521- ('start error', '500 Internal Server Error',
522- [('Content-Type', 'text/json')], ValueError, ('boom, yo',)),
523- ], self.calls)
524+ Equals((
525+ 'start error',
526+ '500 Internal Server Error',
527+ [('Content-Type', 'text/json'), ('X-Oops-Id', '1')],
528+ ValueError, ('boom, yo',))),
529+ ]))
530 self.assertThat(iterated, DocTestMatches(dedent("""\
531 {"oopsid" : "..."}"""), ELLIPSIS))
532
533@@ -249,16 +315,16 @@
534 return "woo"
535 iterated = self.wrap_and_run(inner, config,
536 params=dict(error_render=error_render))
537- self.assertEqual([
538+ self.assertThat(self.calls, MatchesListwise([
539 # First the oops is generated
540- {'id': 1,
541- 'type': 'ValueError',
542- 'url': 'http://example.com/demo?param=value',
543- 'value': u'boom, yo'},
544+ MatchesOOPS({'value': 'boom, yo'}),
545 # Then the middleware responses
546- ('start error', '500 Internal Server Error',
547- [('Content-Type', 'text/html')], ValueError, ('boom, yo',)),
548- ], self.calls)
549+ Equals((
550+ 'start error',
551+ '500 Internal Server Error',
552+ [('Content-Type', 'text/html'), ('X-Oops-Id', '1')],
553+ ValueError, ('boom, yo',))),
554+ ]))
555 self.assertEqual(iterated, 'woo')
556
557 def test_oops_url_in_context(self):
558@@ -266,8 +332,24 @@
559 raise ValueError('boom, yo')
560 config = self.config_for_oopsing()
561 self.wrap_and_run(inner, config)
562- self.assertEqual('http://example.com/demo?param=value',
563- self.calls[0]['url'])
564+ self.assertThat(self.calls, MatchesListwise([
565+ # First the oops is generated - with a url.
566+ MatchesOOPS({'url': 'http://example.com/demo?param=value'}),
567+ # Then the middleware responses
568+ Equals((
569+ 'start error',
570+ '500 Internal Server Error',
571+ [('Content-Type', 'text/html'), ('X-Oops-Id', '1')],
572+ ValueError, ('boom, yo',))),
573+ ]))
574+
575+ def test_error_in_app_context_includes_wsgi_environ(self):
576+ # When the app blows up we attach the environment.
577+ def inner(environ, start_response):
578+ raise ValueError('boom, yo')
579+ config = self.config_for_oopsing(capture_create=True)
580+ self.wrap_and_run(inner, config)
581+ self.assertEqual(self.make_environ(), self.calls[0]['wsgi_environ'])
582
583 def test_start_response_with_just_exc_info_generates_oops(self):
584 def inner(environ, start_response):
585@@ -275,16 +357,74 @@
586 raise ValueError('boom, yo')
587 except ValueError:
588 exc_info = sys.exc_info()
589- start_response('500 FAIL', [], exc_info)
590+ start_response(
591+ '500 FAIL', [('content-type', 'text/plain')], exc_info)
592 yield 'body'
593 config = self.config_for_oopsing()
594+ contents = self.wrap_and_run(inner, config)
595+ # The body from the wrapped app is preserved.
596+ self.assertEqual('body', contents)
597+ # The header though have an X-Oops-Id header added:
598+ headers = [
599+ ('content-type', 'text/plain'),
600+ ('X-Oops-Id', '1')
601+ ]
602+ expected_start_response = \
603+ ('start error', '500 FAIL', headers, ValueError, ('boom, yo',))
604+ self.assertThat(self.calls, MatchesListwise([
605+ # The oops is logged:
606+ MatchesOOPS({'value': 'boom, yo'}),
607+ # And we have forwarded on the wrapped apps start_response call.
608+ Equals(expected_start_response),
609+ ]))
610+
611+ def test_start_response_exc_info_context_includes_wsgi_environ(self):
612+ # When the app handles its own error we still attach the environment.
613+ def inner(environ, start_response):
614+ try:
615+ raise ValueError('boom, yo')
616+ except ValueError:
617+ exc_info = sys.exc_info()
618+ start_response(
619+ '500 FAIL', [('content-type', 'text/plain')], exc_info)
620+ yield 'body'
621+ config = self.config_for_oopsing(capture_create=True)
622 self.wrap_and_run(inner, config)
623- expected_report = {
624- 'id': 1,
625- 'type': 'ValueError',
626- 'url': 'http://example.com/demo?param=value',
627- 'value': u'boom, yo'}
628- expected_start_response = \
629- ('start error', '500 FAIL', [], ValueError, ('boom, yo',))
630- self.assertEqual([expected_report, expected_start_response],
631- self.calls)
632+ self.assertEqual(self.make_environ(), self.calls[0]['wsgi_environ'])
633+
634+ def test_sniff_404_not_published_does_not_error(self):
635+ def inner(environ, start_response):
636+ start_response('404 MISSING', [])
637+ yield 'pretty 404'
638+ contents = self.wrap_and_run(
639+ inner, Config(), params=dict(oops_on_status=['404']))
640+ self.assertEqual('pretty 404', contents)
641+ expected_start_response = ('404 MISSING', [])
642+ self.assertEqual([expected_start_response], self.calls)
643+
644+ def test_sniff_404_published_does_not_error(self):
645+ # Sniffing of unexpected pages to gather oops data doesn't alter the
646+ # wrapped apps page. It also does not add an X-OOPS-ID header because
647+ # client side scripts often look for that to signal errors.
648+ def inner(environ, start_response):
649+ start_response('404 MISSING', [])
650+ yield 'pretty 404'
651+ config = self.config_for_oopsing()
652+ contents = self.wrap_and_run(
653+ inner, config, params=dict(oops_on_status=['404']))
654+ self.assertEqual('pretty 404', contents)
655+ expected_start_response = ('404 MISSING', [])
656+ self.assertThat(self.calls, MatchesListwise([
657+ # An OOPS is logged with the observed response code.
658+ MatchesOOPS({'HTTP_STATUS': '404'}),
659+ Equals(expected_start_response),
660+ ]))
661+
662+ def test_sniff_oops_context_includes_wsgi_environ(self):
663+ def inner(environ, start_response):
664+ start_response('404 MISSING', [])
665+ yield 'pretty 404'
666+ config = self.config_for_oopsing(capture_create=True)
667+ self.wrap_and_run(
668+ inner, config, params=dict(oops_on_status=['404']))
669+ self.assertEqual(self.make_environ(), self.calls[0]['wsgi_environ'])
670
671=== modified file 'setup.py'
672--- setup.py 2011-08-22 07:07:16 +0000
673+++ setup.py 2011-08-24 01:57:31 +0000
674@@ -23,7 +23,7 @@
675 os.path.join(os.path.dirname(__file__), 'README'), 'rb').read()
676
677 setup(name="oops_wsgi",
678- version="0.0.2",
679+ version="0.0.3",
680 description=\
681 "OOPS wsgi middleware.",
682 long_description=description,

Subscribers

People subscribed via source and target branches

to all changes: