150
148
self.assertEqual(self.browser.url, dummy_root + '+logout')
151
149
self.assertEqual(self.browser.contents,
152
150
'This is a dummy destination.\n')
155
class TestOopsMiddleware(TestCase):
158
super(TestOopsMiddleware, self).setUp()
159
self.start_response_called = False
161
def assertContainsRe(self, haystack, needle_re, flags=0):
162
"""Assert that a contains something matching a regular expression."""
163
# There is: self.assertTextMatchesExpressionIgnoreWhitespace
164
# but it does weird things with whitespace, and gives
165
# unhelpful error messages when it fails, so this is copied
167
if not re.search(needle_re, haystack, flags):
168
if '\n' in haystack or len(haystack) > 60:
169
# a long string, format it in a more readable way
170
raise AssertionError(
171
'pattern "%s" not found in\n"""\\\n%s"""\n'
172
% (needle_re, haystack))
174
raise AssertionError('pattern "%s" not found in "%s"'
175
% (needle_re, haystack))
177
def catchLogEvents(self):
178
"""Any log events that are triggered get written to self.log_stream"""
179
logger = logging.getLogger('lp-loggerhead')
180
logger.setLevel(logging.DEBUG)
181
self.log_stream = cStringIO.StringIO()
182
handler = logging.StreamHandler(self.log_stream)
183
handler.setLevel(logging.DEBUG)
184
logger.addHandler(handler)
185
self.addCleanup(logger.removeHandler, handler)
187
def runtime_failing_app(self, environ, start_response):
190
raise RuntimeError('just a generic runtime error.')
192
def socket_failing_app(self, environ, start_response):
195
raise socket.error(errno.EPIPE, 'Connection closed')
197
def logging_start_response(self, status, response_headers, exc_info=None):
198
self._response_chunks = []
200
self._response_chunks.append(chunk)
201
self.start_response_called = True
204
def success_app(self, environ, start_response):
205
writer = start_response('200 OK', {})
206
writer('Successfull\n')
209
def failing_start_response(self, status, response_headers, exc_info=None):
210
def fail_write(chunk):
211
raise socket.error(errno.EPIPE, 'Connection closed')
212
self.start_response_called = True
215
def multi_yielding_app(self, environ, start_response):
216
writer = start_response('200 OK', {})
219
yield 'to give to the user\n'
221
def no_body_app(self, environ, start_response):
222
writer = start_response('200 OK', {})
225
def _get_default_environ(self):
226
return {'wsgi.version': (1, 0),
227
'wsgi.url_scheme': 'http',
228
'PATH_INFO': '/test/path',
229
'REQUEST_METHOD': 'GET',
230
'SERVER_NAME': 'localhost',
231
'SERVER_PORT': '8080',
234
def wrap_and_run(self, app, failing_write=False):
235
app = oops_middleware(app)
236
# Just random env data, rather than setting up a whole wsgi stack just
237
# to pass in values for this dict
238
environ = self._get_default_environ()
240
result = list(app(environ, self.failing_start_response))
242
result = list(app(environ, self.logging_start_response))
245
def test_exception_triggers_oops(self):
246
res = self.wrap_and_run(self.runtime_failing_app)
247
# After the exception was raised, we should also have gotten an oops
249
self.assertEqual(1, len(self.oopses))
250
oops = self.oopses[0]
251
self.assertEqual('RuntimeError', oops['type'])
252
# runtime_failing_app doesn't call start_response, but oops_middleware
253
# does because it tries to send the OOPS information to the user.
254
self.assertTrue(self.start_response_called)
255
self.assertEqual(_oops_html_template % {'oopsid': oops['id']},
256
''.join(self._response_chunks))
258
def test_ignores_socket_exceptions(self):
259
self.catchLogEvents()
260
res = self.wrap_and_run(self.socket_failing_app)
261
self.assertEqual(0, len(self.oopses))
262
self.assertContainsRe(self.log_stream.getvalue(),
263
'Caught socket exception from <unknown>:.*Connection closed')
264
# start_response doesn't get called because the app fails first,
265
# and oops_middleware knows not to do anything with a closed socket.
266
self.assertFalse(self.start_response_called)
268
def test_ignores_writer_failures(self):
269
self.catchLogEvents()
270
res = self.wrap_and_run(self.success_app, failing_write=True)
271
self.assertEqual(0, len(self.oopses))
272
self.assertContainsRe(self.log_stream.getvalue(),
273
'Caught socket exception from <unknown>:.*Connection closed')
274
# success_app calls start_response, so this should get passed on.
275
self.assertTrue(self.start_response_called)
277
def test_stopping_early_no_oops(self):
279
# If content is being streamed, and the pipe closes, we'll get a
280
# 'GeneratorExit', because it is closed before finishing. This doesn't
281
# need to become an OOPS.
282
self.catchLogEvents()
283
app = oops_middleware(self.multi_yielding_app)
284
environ = self._get_default_environ()
285
result = app(environ, self.logging_start_response)
286
self.assertEqual('content\n', result.next())
287
# At this point, we intentionally kill the app and the response, so
288
# that they will get GeneratorExit
290
self.assertEqual([], self.oopses)
291
self.assertContainsRe(self.log_stream.getvalue(),
292
'Caught GeneratorExit from <unknown>')
293
# Body content was yielded, we must have called start_response
294
self.assertTrue(self.start_response_called)
296
def test_no_body_calls_start_response(self):
297
# See bug #732481, even if we don't have a body, if we have headers to
298
# send, we must call start_response
299
result = self.wrap_and_run(self.no_body_app)
300
self.assertEqual([], result)
301
self.assertTrue(self.start_response_called)
302
# Output content is empty because of no_body_app
303
self.assertEqual('', ''.join(self._response_chunks))