Merge lp:~lifeless/python-oops-wsgi/foru1 into lp:python-oops-wsgi
- foru1
- Merge into trunk
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 | ||||||||||||||||
Related bugs: |
|
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.
- 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, |
There is an errant \ on line 284 of the diff.