Merge lp:~thumper/launchpad/more-careful-network-service-usage into lp:launchpad/db-devel

Proposed by Tim Penhey on 2010-04-30
Status: Merged
Merged at revision: not available
Proposed branch: lp:~thumper/launchpad/more-careful-network-service-usage
Merge into: lp:launchpad/db-devel
Diff against target: 465 lines (+224/-17)
10 files modified
lib/canonical/launchpad/webapp/errorlog.py (+9/-1)
lib/canonical/launchpad/webapp/interfaces.py (+5/-0)
lib/lp/code/model/branchmergeproposaljob.py (+9/-0)
lib/lp/code/model/tests/test_branchmergeproposaljobs.py (+98/-1)
lib/lp/code/model/tests/test_diff.py (+2/-4)
lib/lp/codehosting/scanner/tests/test_bzrsync.py (+4/-2)
lib/lp/services/job/runner.py (+22/-7)
lib/lp/services/job/tests/test_runner.py (+63/-0)
lib/lp/testing/__init__.py (+11/-1)
lib/lp/testing/tests/test_fixture.py (+1/-1)
To merge this branch: bzr merge lp:~thumper/launchpad/more-careful-network-service-usage
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle 2010-04-30 Approve on 2010-05-03
Björn Tillenius (community) 2010-04-30 Approve on 2010-04-30
Review via email: mp+24469@code.launchpad.net

Commit Message

Add missing operation descriptions for the jobs, and catch errors that might arise during emailing users about errors.

Description of the Change

This branch adds a default getOperationDescription to the BaseRunnableJob, and also adds extra exception handling around the handing of other exceptions.

If the run job raises an error, we attempt to notify the recipients. However, as we have found with staging, that can fail. The extra try/except block catches this failures to notify about failures, but it just logs the error and makes an oops.

Extra tests were added for the other job types to make sure they have a sensible getOperationDescription.

To post a comment you must log in.
Michael Hudson-Doyle (mwhudson) wrote :

As mentioned on IRC, please us logger.exception() instead of using logger.error(e).

Otherwise fine.

review: Approve
Björn Tillenius (bjornt) wrote :

On Fri, Apr 30, 2010 at 05:26:29AM -0000, Tim Penhey wrote:
> === modified file 'lib/lp/services/job/runner.py'
> --- lib/lp/services/job/runner.py 2010-04-06 03:37:16 +0000
> +++ lib/lp/services/job/runner.py 2010-04-30 05:22:33 +0000

> @@ -181,13 +184,24 @@
> with self.error_utility.oopsMessage(
> dict(job.getOopsVars())):
> try:
> - self.logger.debug('Running %r', job)
> - self.runJob(job)
> - except job.user_error_types, e:
> - job.notifyUserError(e)
> - except Exception:
> + try:
> + self.logger.debug('Running %r', job)
> + self.runJob(job)
> + except job.user_error_types, e:
> + job.notifyUserError(e)
> + except Exception:
> + info = sys.exc_info()
> + return self._doOops(job, info)
> + except Exception, e:
> + # This only happens if sending attempting to notify users
> + # about errors fails for some reason (like a misconfigured
> + # email server).
> + self.logger.exception(e)
> info = sys.exc_info()
> - return self._doOops(job, info)
> + self.error_utility.raising(info)
> + oops = self.error_utility.getLastOopsReport()
> + # Returning the oops says something went wrong.
> + return oops

As discussed on IRC, this new exception handling is untested. To avoid
things breaking, and to avoid people getting tempted into refactoring
the outer exception handling into using self._doOops(), we need two
tests; one where job.notifyUserError() errors out, and one where
job.notifyOops() errors out.

Since it's EOD for you now, I'd be willing to let this branch land
without tests (since it won't break anything not already broken), so
that you can QA it more easily on Monday. Given that you promise to
write the tests on Monday, of course.

    vote approve

--
Björn Tillenius | https://launchpad.net/~bjornt

review: Approve
Tim Penhey (thumper) wrote :

I've updated the test cases for the job runner to confirm that the
errors are handled as expected and that oopses are generated
when we expect them to be.

As a part of this change I have updated the base launchpad TestCase to record
oopses that are generated during the test execution.

These are available as self.oopses and is a list.

This is done by adding an object event to the global error reporting utility
class so that when new oopses are generated and saved it notifies listeners.
The test case starts listening in setUp and stops listening in tearDown thanks
to Fixture code that was in the branch scanner. This code has been moved into
the testing module as no code was currently using it (due to a past change in
the scanner). The tests for the fixtures were also moved into the testing
module.

This also fixes bug 567689, and I did a drive by fix of a spurious test failure,
bug 567257.

1=== modified file 'lib/canonical/launchpad/webapp/errorlog.py'
2--- lib/canonical/launchpad/webapp/errorlog.py 2009-11-26 21:25:17 +0000
3+++ lib/canonical/launchpad/webapp/errorlog.py 2010-05-02 22:58:46 +0000
4@@ -20,7 +20,9 @@
5 import urllib
6
7 import pytz
8+from zope.component.interfaces import ObjectEvent
9 from zope.error.interfaces import IErrorReportingUtility
10+from zope.event import notify
11 from zope.exceptions.exceptionformatter import format_exception
12 from zope.interface import implements
13 from zope.publisher.interfaces.xmlrpc import IXMLRPCRequest
14@@ -35,7 +37,7 @@
15 get_request_statements, get_request_duration,
16 soft_timeout_expired)
17 from canonical.launchpad.webapp.interfaces import (
18- IErrorReport, IErrorReportRequest)
19+ IErrorReport, IErrorReportEvent, IErrorReportRequest)
20 from canonical.launchpad.webapp.opstats import OpStats
21
22 UTC = pytz.utc
23@@ -130,6 +132,11 @@
24 *(int(elem) for elem in re.findall('[0-9]+', datestring)[:7]))
25
26
27+class ErrorReportEvent(ObjectEvent):
28+ """A new error report has been created."""
29+ implements(IErrorReportEvent)
30+
31+
32 class ErrorReport:
33 implements(IErrorReport)
34
35@@ -426,6 +433,7 @@
36 if self.copy_to_zlog:
37 self._do_copy_to_zlog(
38 entry.time, entry.type, entry.url, info, entry.id)
39+ notify(ErrorReportEvent(entry))
40
41 def _makeErrorReport(self, info, request=None, now=None,
42 informational=False):
43
44=== modified file 'lib/canonical/launchpad/webapp/interfaces.py'
45--- lib/canonical/launchpad/webapp/interfaces.py 2010-04-01 17:20:31 +0000
46+++ lib/canonical/launchpad/webapp/interfaces.py 2010-05-02 22:56:22 +0000
47@@ -8,6 +8,7 @@
48 import logging
49
50 import zope.app.publication.interfaces
51+from zope.component.interfaces import IObjectEvent
52 from zope.interface import Interface, Attribute, implements
53 from zope.app.security.interfaces import (
54 IAuthentication, IPrincipal, IPrincipalSource)
55@@ -674,6 +675,10 @@
56 """
57
58
59+class IErrorReportEvent(IObjectEvent):
60+ """A new error report has been created."""
61+
62+
63 class IErrorReport(Interface):
64 id = TextLine(description=u"The name of this error report.")
65 type = TextLine(description=u"The type of the exception that occurred.")
66
67=== modified file 'lib/lp/code/model/tests/test_diff.py'
68--- lib/lp/code/model/tests/test_diff.py 2010-04-26 00:24:26 +0000
69+++ lib/lp/code/model/tests/test_diff.py 2010-05-03 00:02:07 +0000
70@@ -260,10 +260,10 @@
71 def test_fromFile_withError(self):
72 # If the diff is formatted such that generating the diffstat fails, we
73 # want to record an oops but continue.
74- last_oops_id = errorlog.globalErrorUtility.lastid
75 diff_bytes = "not a real diff"
76 diff = Diff.fromFile(StringIO(diff_bytes), len(diff_bytes))
77- self.assertNotEqual(last_oops_id, errorlog.globalErrorUtility.lastid)
78+ oops = self.oopses[0]
79+ self.assertEqual('MalformedPatchHeader', oops.type)
80 self.assertIs(None, diff.diffstat)
81 self.assertIs(None, diff.added_lines_count)
82 self.assertIs(None, diff.removed_lines_count)
83
84=== modified file 'lib/lp/services/job/tests/test_runner.py'
85--- lib/lp/services/job/tests/test_runner.py 2010-04-06 00:01:24 +0000
86+++ lib/lp/services/job/tests/test_runner.py 2010-05-03 01:56:20 +0000
87@@ -76,6 +76,37 @@
88 raise RaisingJobException(self.message)
89
90
91+class RaisingJobUserError(NullJob):
92+ """A job that raises when it runs."""
93+
94+ user_error_types = (RaisingJobException, )
95+
96+ def run(self):
97+ raise RaisingJobException(self.message)
98+
99+
100+class RaisingJobRaisingNotifyOops(NullJob):
101+ """A job that raises when it runs, and when getting oops recipients."""
102+
103+ def run(self):
104+ raise RaisingJobException(self.message)
105+
106+ def notifyOops(self, oops):
107+ raise RaisingJobException('oops notifying oops')
108+
109+
110+class RaisingJobRaisingNotifyUserError(NullJob):
111+ """A job that raises when it runs, and when notifying user errors."""
112+
113+ user_error_types = (RaisingJobException, )
114+
115+ def run(self):
116+ raise RaisingJobException(self.message)
117+
118+ def notifyUserError(self, error):
119+ raise RaisingJobException('oops notifying users')
120+
121+
122 class TestJobRunner(TestCaseWithFactory):
123 """Ensure JobRunner behaves as expected."""
124
125@@ -254,6 +285,38 @@
126 transaction.abort()
127 self.assertEqual(JobStatus.FAILED, job.job.status)
128
129+ def test_runJobHandleErrors_oops_generated(self):
130+ """The handle errors method records an oops for raised errors."""
131+ job = RaisingJob('boom')
132+ runner = JobRunner([job])
133+ runner.runJobHandleError(job)
134+ self.assertEqual(1, len(self.oopses))
135+
136+ def test_runJobHandleErrors_user_error_no_oops(self):
137+ """If the job raises a user error, there is no oops."""
138+ job = RaisingJobUserError('boom')
139+ runner = JobRunner([job])
140+ runner.runJobHandleError(job)
141+ self.assertEqual(0, len(self.oopses))
142+
143+ def test_runJobHandleErrors_oops_generated_notify_fails(self):
144+ """A second oops is logged if the notification of the oops fails."""
145+ job = RaisingJobRaisingNotifyOops('boom')
146+ runner = JobRunner([job])
147+ runner.runJobHandleError(job)
148+ self.assertEqual(2, len(self.oopses))
149+
150+ def test_runJobHandleErrors_oops_generated_user_notify_fails(self):
151+ """A second oops is logged if the notification of the oops fails.
152+
153+ In this test case the error is a user expected error, so the
154+ notifyUserError is called, and in this case the notify raises too.
155+ """
156+ job = RaisingJobRaisingNotifyUserError('boom')
157+ runner = JobRunner([job])
158+ runner.runJobHandleError(job)
159+ self.assertEqual(1, len(self.oopses))
160+
161
162 class StuckJob(BaseRunnableJob):
163 """Simulation of a job that stalls."""
164
165=== modified file 'lib/lp/testing/__init__.py'
166--- lib/lp/testing/__init__.py 2010-04-27 04:46:58 +0000
167+++ lib/lp/testing/__init__.py 2010-05-03 00:01:25 +0000
168@@ -72,7 +72,7 @@
169
170 from windmill.authoring import WindmillTestClient
171
172-from zope.component import getUtility
173+from zope.component import adapter, getUtility
174 import zope.event
175 from zope.interface.verify import verifyClass, verifyObject
176 from zope.security.proxy import (
177@@ -81,6 +81,7 @@
178
179 from canonical.launchpad.webapp import errorlog
180 from canonical.config import config
181+from canonical.launchpad.webapp.errorlog import ErrorReportEvent
182 from canonical.launchpad.webapp.interaction import ANONYMOUS
183 from canonical.launchpad.webapp.interfaces import ILaunchBag
184 from canonical.launchpad.windmill.testing import constants
185@@ -95,6 +96,7 @@
186 from lp.testing._tales import test_tales
187 from lp.testing._webservice import (
188 launchpadlib_credentials_for, launchpadlib_for, oauth_access_token_for)
189+from lp.testing.fixture import ZopeEventHandlerFixture
190
191 # zope.exception demands more of frame objects than twisted.python.failure
192 # provides in its fake frames. This is enough to make it work with them
193@@ -382,6 +384,14 @@
194 testtools.TestCase.setUp(self)
195 from lp.testing.factory import ObjectFactory
196 self.factory = ObjectFactory()
197+ # Record the oopses generated during the test run.
198+ self.oopses = []
199+ self.installFixture(ZopeEventHandlerFixture(self._recordOops))
200+
201+ @adapter(ErrorReportEvent)
202+ def _recordOops(self, event):
203+ """Add the oops to the testcase's list."""
204+ self.oopses.append(event.object)
205
206 def assertStatementCount(self, expected_count, function, *args, **kwargs):
207 """Assert that the expected number of SQL statements occurred.
208
209=== renamed file 'lib/lp/codehosting/scanner/fixture.py' => 'lib/lp/testing/fixture.py'
210=== renamed file 'lib/lp/codehosting/scanner/tests/test_fixture.py' => 'lib/lp/testing/tests/test_fixture.py'
211--- lib/lp/codehosting/scanner/tests/test_fixture.py 2009-06-30 16:56:07 +0000
212+++ lib/lp/testing/tests/test_fixture.py 2010-05-02 23:39:28 +0000
213@@ -9,7 +9,7 @@
214
215 from zope.interface import implements
216
217-from lp.codehosting.scanner.fixture import (
218+from lp.testing.fixture import (
219 Fixtures, FixtureWithCleanup, IFixture, run_with_fixture, with_fixture)
220 from lp.testing import TestCase
221
Michael Hudson-Doyle (mwhudson) wrote :

All the new stuff looks great to me. There *may* be oops-related APIs that aren't needed any more, but that can wait.

review: Approve
Björn Tillenius (bjornt) wrote :

Those changes look good. firing off an event is a good idea, and the tests look good.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/webapp/errorlog.py'
2--- lib/canonical/launchpad/webapp/errorlog.py 2009-11-26 21:25:17 +0000
3+++ lib/canonical/launchpad/webapp/errorlog.py 2010-05-03 14:15:42 +0000
4@@ -20,7 +20,9 @@
5 import urllib
6
7 import pytz
8+from zope.component.interfaces import ObjectEvent
9 from zope.error.interfaces import IErrorReportingUtility
10+from zope.event import notify
11 from zope.exceptions.exceptionformatter import format_exception
12 from zope.interface import implements
13 from zope.publisher.interfaces.xmlrpc import IXMLRPCRequest
14@@ -35,7 +37,7 @@
15 get_request_statements, get_request_duration,
16 soft_timeout_expired)
17 from canonical.launchpad.webapp.interfaces import (
18- IErrorReport, IErrorReportRequest)
19+ IErrorReport, IErrorReportEvent, IErrorReportRequest)
20 from canonical.launchpad.webapp.opstats import OpStats
21
22 UTC = pytz.utc
23@@ -130,6 +132,11 @@
24 *(int(elem) for elem in re.findall('[0-9]+', datestring)[:7]))
25
26
27+class ErrorReportEvent(ObjectEvent):
28+ """A new error report has been created."""
29+ implements(IErrorReportEvent)
30+
31+
32 class ErrorReport:
33 implements(IErrorReport)
34
35@@ -426,6 +433,7 @@
36 if self.copy_to_zlog:
37 self._do_copy_to_zlog(
38 entry.time, entry.type, entry.url, info, entry.id)
39+ notify(ErrorReportEvent(entry))
40
41 def _makeErrorReport(self, info, request=None, now=None,
42 informational=False):
43
44=== modified file 'lib/canonical/launchpad/webapp/interfaces.py'
45--- lib/canonical/launchpad/webapp/interfaces.py 2010-04-01 17:20:31 +0000
46+++ lib/canonical/launchpad/webapp/interfaces.py 2010-05-03 14:15:42 +0000
47@@ -8,6 +8,7 @@
48 import logging
49
50 import zope.app.publication.interfaces
51+from zope.component.interfaces import IObjectEvent
52 from zope.interface import Interface, Attribute, implements
53 from zope.app.security.interfaces import (
54 IAuthentication, IPrincipal, IPrincipalSource)
55@@ -674,6 +675,10 @@
56 """
57
58
59+class IErrorReportEvent(IObjectEvent):
60+ """A new error report has been created."""
61+
62+
63 class IErrorReport(Interface):
64 id = TextLine(description=u"The name of this error report.")
65 type = TextLine(description=u"The type of the exception that occurred.")
66
67=== modified file 'lib/lp/code/model/branchmergeproposaljob.py'
68--- lib/lp/code/model/branchmergeproposaljob.py 2010-04-23 08:33:14 +0000
69+++ lib/lp/code/model/branchmergeproposaljob.py 2010-05-03 14:15:42 +0000
70@@ -471,6 +471,9 @@
71 commenter = self.code_review_comment.message.owner
72 return [format_address_for_person(commenter)]
73
74+ def getOperationDescription(self):
75+ return 'emailing a code review comment'
76+
77
78 class ReviewRequestedEmailJob(BranchMergeProposalJobDerived):
79 """Send email to the reviewer telling them to review the proposal.
80@@ -533,6 +536,9 @@
81 recipients.append(format_address_for_person(self.requester))
82 return recipients
83
84+ def getOperationDescription(self):
85+ return 'emailing a reviewer requesting a review'
86+
87
88 class MergeProposalUpdatedEmailJob(BranchMergeProposalJobDerived):
89 """Send email to the subscribers informing them of updated fields.
90@@ -598,6 +604,9 @@
91 recipients.append(format_address_for_person(self.editor))
92 return recipients
93
94+ def getOperationDescription(self):
95+ return 'emailing subscribers about merge proposal changes'
96+
97
98 class BranchMergeProposalJobFactory:
99 """Construct a derived merge proposal job for a BranchMergeProposalJob."""
100
101=== modified file 'lib/lp/code/model/tests/test_branchmergeproposaljobs.py'
102--- lib/lp/code/model/tests/test_branchmergeproposaljobs.py 2010-04-26 00:24:26 +0000
103+++ lib/lp/code/model/tests/test_branchmergeproposaljobs.py 2010-05-03 14:15:42 +0000
104@@ -23,7 +23,11 @@
105 from lp.code.adapters.branch import BranchMergeProposalDelta
106 from lp.code.interfaces.branchmergeproposal import (
107 IBranchMergeProposalJob, IBranchMergeProposalJobSource,
108- IMergeProposalCreatedJob, IUpdatePreviewDiffJobSource,
109+ ICodeReviewCommentEmailJob, ICodeReviewCommentEmailJobSource,
110+ IMergeProposalCreatedJob, IMergeProposalUpdatedEmailJob,
111+ IMergeProposalUpdatedEmailJobSource,
112+ IReviewRequestedEmailJob, IReviewRequestedEmailJobSource,
113+ IUpdatePreviewDiffJob, IUpdatePreviewDiffJobSource,
114 )
115 from lp.code.model.branchmergeproposaljob import (
116 BranchMergeProposalJob, BranchMergeProposalJobDerived,
117@@ -87,6 +91,13 @@
118 verifyObject(IMergeProposalCreatedJob, job)
119 verifyObject(IBranchMergeProposalJob, job)
120
121+ def test_getOperationDescription(self):
122+ bmp = self.factory.makeBranchMergeProposal()
123+ job = MergeProposalCreatedJob.create(bmp)
124+ self.assertTrue(
125+ job.getOperationDescription().startswith(
126+ 'notifying people about the proposal to merge'))
127+
128 def checkDiff(self, diff):
129 self.assertNotIn('+bar', diff.diff.text)
130 self.assertIn('+qux', diff.diff.text)
131@@ -144,6 +155,20 @@
132 """UpdatePreviewDiffJob implements IUpdatePreviewDiffJobSource."""
133 verifyObject(IUpdatePreviewDiffJobSource, UpdatePreviewDiffJob)
134
135+ def test_providesInterface(self):
136+ """MergeProposalCreatedJob provides the expected interfaces."""
137+ bmp = self.factory.makeBranchMergeProposal()
138+ job = UpdatePreviewDiffJob.create(bmp)
139+ verifyObject(IUpdatePreviewDiffJob, job)
140+ verifyObject(IBranchMergeProposalJob, job)
141+
142+ def test_getOperationDescription(self):
143+ bmp = self.factory.makeBranchMergeProposal()
144+ job = UpdatePreviewDiffJob.create(bmp)
145+ self.assertEqual(
146+ 'generating the diff for a merge proposal',
147+ job.getOperationDescription())
148+
149 def test_run(self):
150 self.useBzrBranches(direct_database=True)
151 bmp = self.createExampleMerge()[0]
152@@ -345,5 +370,77 @@
153 self.assertIsInstance(job, MergeProposalUpdatedEmailJob)
154
155
156+class TestCodeReviewCommentEmailJob(TestCaseWithFactory):
157+
158+ layer = LaunchpadZopelessLayer
159+
160+ def test_implement_interface(self):
161+ """UpdatePreviewDiffJob implements IUpdatePreviewDiffJobSource."""
162+ verifyObject(
163+ ICodeReviewCommentEmailJobSource, CodeReviewCommentEmailJob)
164+
165+ def test_providesInterface(self):
166+ """CodeReviewCommentEmailJob provides the expected interfaces."""
167+ comment = self.factory.makeCodeReviewComment()
168+ job = CodeReviewCommentEmailJob.create(comment)
169+ verifyObject(ICodeReviewCommentEmailJob, job)
170+ verifyObject(IBranchMergeProposalJob, job)
171+
172+ def test_getOperationDescription(self):
173+ comment = self.factory.makeCodeReviewComment()
174+ job = CodeReviewCommentEmailJob.create(comment)
175+ self.assertEqual(
176+ 'emailing a code review comment',
177+ job.getOperationDescription())
178+
179+
180+class TestReviewRequestedEmailJob(TestCaseWithFactory):
181+
182+ layer = LaunchpadZopelessLayer
183+
184+ def test_implement_interface(self):
185+ """UpdatePreviewDiffJob implements IUpdatePreviewDiffJobSource."""
186+ verifyObject(IReviewRequestedEmailJobSource, ReviewRequestedEmailJob)
187+
188+ def test_providesInterface(self):
189+ """MergeProposalCreatedJob provides the expected interfaces."""
190+ request = self.factory.makeCodeReviewVoteReference()
191+ job = ReviewRequestedEmailJob.create(request)
192+ verifyObject(IReviewRequestedEmailJob, job)
193+ verifyObject(IBranchMergeProposalJob, job)
194+
195+ def test_getOperationDescription(self):
196+ request = self.factory.makeCodeReviewVoteReference()
197+ job = ReviewRequestedEmailJob.create(request)
198+ self.assertEqual(
199+ 'emailing a reviewer requesting a review',
200+ job.getOperationDescription())
201+
202+
203+class TestMergeProposalUpdatedEmailJob(TestCaseWithFactory):
204+
205+ layer = LaunchpadZopelessLayer
206+
207+ def test_class_implements_interface(self):
208+ verifyObject(
209+ IMergeProposalUpdatedEmailJobSource, MergeProposalUpdatedEmailJob)
210+
211+ def test_providesInterface(self):
212+ """MergeProposalUpdatedEmailJob provides the expected interfaces."""
213+ bmp = self.factory.makeBranchMergeProposal()
214+ job = MergeProposalUpdatedEmailJob.create(
215+ bmp, 'change', bmp.registrant)
216+ verifyObject(IMergeProposalUpdatedEmailJob, job)
217+ verifyObject(IBranchMergeProposalJob, job)
218+
219+ def test_getOperationDescription(self):
220+ bmp = self.factory.makeBranchMergeProposal()
221+ job = MergeProposalUpdatedEmailJob.create(
222+ bmp, 'change', bmp.registrant)
223+ self.assertEqual(
224+ 'emailing subscribers about merge proposal changes',
225+ job.getOperationDescription())
226+
227+
228 def test_suite():
229 return unittest.TestLoader().loadTestsFromName(__name__)
230
231=== modified file 'lib/lp/code/model/tests/test_diff.py'
232--- lib/lp/code/model/tests/test_diff.py 2010-04-30 03:10:17 +0000
233+++ lib/lp/code/model/tests/test_diff.py 2010-05-03 14:15:42 +0000
234@@ -260,12 +260,10 @@
235 def test_fromFile_withError(self):
236 # If the diff is formatted such that generating the diffstat fails, we
237 # want to record an oops but continue.
238- last_oops_id = errorlog.globalErrorUtility.lastid
239 diff_bytes = "not a real diff"
240 diff = Diff.fromFile(StringIO(diff_bytes), len(diff_bytes))
241- # XXX MichaelHudson, 2010-04-29, bug=567257: This test fails
242- # intermittently when run close to midnight.
243- #self.assertNotEqual(last_oops_id, errorlog.globalErrorUtility.lastid)
244+ oops = self.oopses[0]
245+ self.assertEqual('MalformedPatchHeader', oops.type)
246 self.assertIs(None, diff.diffstat)
247 self.assertIs(None, diff.added_lines_count)
248 self.assertIs(None, diff.removed_lines_count)
249
250=== modified file 'lib/lp/codehosting/scanner/tests/test_bzrsync.py'
251--- lib/lp/codehosting/scanner/tests/test_bzrsync.py 2010-04-28 05:40:02 +0000
252+++ lib/lp/codehosting/scanner/tests/test_bzrsync.py 2010-05-03 14:15:42 +0000
253@@ -59,14 +59,16 @@
254 LOG = "Log message"
255
256 def setUp(self):
257- TestCaseWithTransport.setUp(self)
258- TestCaseWithFactory.setUp(self)
259+ super(BzrSyncTestCase, self).setUp()
260 self.disable_directory_isolation()
261 self.useBzrBranches(direct_database=True)
262 self.lp_db_user = config.launchpad.dbuser
263 self.makeFixtures()
264 LaunchpadZopelessLayer.switchDbUser(config.branchscanner.dbuser)
265
266+ def tearDown(self):
267+ super(BzrSyncTestCase, self).tearDown()
268+
269 def makeFixtures(self):
270 """Makes test fixtures before we switch to the scanner db user."""
271 self.db_branch, self.bzr_tree = self.create_branch_and_tree(
272
273=== modified file 'lib/lp/services/job/runner.py'
274--- lib/lp/services/job/runner.py 2010-04-06 03:37:16 +0000
275+++ lib/lp/services/job/runner.py 2010-05-03 14:15:42 +0000
276@@ -72,6 +72,9 @@
277 """Return a list of email-ids to notify about oopses."""
278 return self.getErrorRecipients()
279
280+ def getOperationDescription(self):
281+ return 'unspecified operation'
282+
283 def getErrorRecipients(self):
284 """Return a list of email-ids to notify about user errors."""
285 return []
286@@ -155,8 +158,8 @@
287
288 try:
289 job.run()
290- except Exception, e:
291- self.logger.error(e)
292+ except Exception:
293+ self.logger.exception("Job execution raised an exception.")
294 transaction.abort()
295 job.fail()
296 # Record the failure.
297@@ -181,13 +184,25 @@
298 with self.error_utility.oopsMessage(
299 dict(job.getOopsVars())):
300 try:
301- self.logger.debug('Running %r', job)
302- self.runJob(job)
303- except job.user_error_types, e:
304- job.notifyUserError(e)
305+ try:
306+ self.logger.debug('Running %r', job)
307+ self.runJob(job)
308+ except job.user_error_types, e:
309+ job.notifyUserError(e)
310+ except Exception:
311+ info = sys.exc_info()
312+ return self._doOops(job, info)
313 except Exception:
314+ # This only happens if sending attempting to notify users
315+ # about errors fails for some reason (like a misconfigured
316+ # email server).
317+ self.logger.exception(
318+ "Failed to notify users about a failure.")
319 info = sys.exc_info()
320- return self._doOops(job, info)
321+ self.error_utility.raising(info)
322+ oops = self.error_utility.getLastOopsReport()
323+ # Returning the oops says something went wrong.
324+ return oops
325
326 def _doOops(self, job, info):
327 """Report an OOPS for the provided job and info.
328
329=== modified file 'lib/lp/services/job/tests/test_runner.py'
330--- lib/lp/services/job/tests/test_runner.py 2010-04-06 00:01:24 +0000
331+++ lib/lp/services/job/tests/test_runner.py 2010-05-03 14:15:42 +0000
332@@ -76,6 +76,37 @@
333 raise RaisingJobException(self.message)
334
335
336+class RaisingJobUserError(NullJob):
337+ """A job that raises a user error when it runs."""
338+
339+ user_error_types = (RaisingJobException, )
340+
341+ def run(self):
342+ raise RaisingJobException(self.message)
343+
344+
345+class RaisingJobRaisingNotifyOops(NullJob):
346+ """A job that raises when it runs, and when calling notifyOops."""
347+
348+ def run(self):
349+ raise RaisingJobException(self.message)
350+
351+ def notifyOops(self, oops):
352+ raise RaisingJobException('oops notifying oops')
353+
354+
355+class RaisingJobRaisingNotifyUserError(NullJob):
356+ """A job that raises when it runs, and when notifying user errors."""
357+
358+ user_error_types = (RaisingJobException, )
359+
360+ def run(self):
361+ raise RaisingJobException(self.message)
362+
363+ def notifyUserError(self, error):
364+ raise RaisingJobException('oops notifying users')
365+
366+
367 class TestJobRunner(TestCaseWithFactory):
368 """Ensure JobRunner behaves as expected."""
369
370@@ -254,6 +285,38 @@
371 transaction.abort()
372 self.assertEqual(JobStatus.FAILED, job.job.status)
373
374+ def test_runJobHandleErrors_oops_generated(self):
375+ """The handle errors method records an oops for raised errors."""
376+ job = RaisingJob('boom')
377+ runner = JobRunner([job])
378+ runner.runJobHandleError(job)
379+ self.assertEqual(1, len(self.oopses))
380+
381+ def test_runJobHandleErrors_user_error_no_oops(self):
382+ """If the job raises a user error, there is no oops."""
383+ job = RaisingJobUserError('boom')
384+ runner = JobRunner([job])
385+ runner.runJobHandleError(job)
386+ self.assertEqual(0, len(self.oopses))
387+
388+ def test_runJobHandleErrors_oops_generated_notify_fails(self):
389+ """A second oops is logged if the notification of the oops fails."""
390+ job = RaisingJobRaisingNotifyOops('boom')
391+ runner = JobRunner([job])
392+ runner.runJobHandleError(job)
393+ self.assertEqual(2, len(self.oopses))
394+
395+ def test_runJobHandleErrors_oops_generated_user_notify_fails(self):
396+ """A second oops is logged if the notification of the oops fails.
397+
398+ In this test case the error is a user expected error, so the
399+ notifyUserError is called, and in this case the notify raises too.
400+ """
401+ job = RaisingJobRaisingNotifyUserError('boom')
402+ runner = JobRunner([job])
403+ runner.runJobHandleError(job)
404+ self.assertEqual(1, len(self.oopses))
405+
406
407 class StuckJob(BaseRunnableJob):
408 """Simulation of a job that stalls."""
409
410=== modified file 'lib/lp/testing/__init__.py'
411--- lib/lp/testing/__init__.py 2010-04-30 12:33:16 +0000
412+++ lib/lp/testing/__init__.py 2010-05-03 14:15:42 +0000
413@@ -73,7 +73,7 @@
414
415 from windmill.authoring import WindmillTestClient
416
417-from zope.component import getUtility
418+from zope.component import adapter, getUtility
419 import zope.event
420 from zope.interface.verify import verifyClass, verifyObject
421 from zope.security.proxy import (
422@@ -83,6 +83,7 @@
423 from canonical.launchpad.webapp import canonical_url, errorlog
424 from canonical.launchpad.webapp.servers import WebServiceTestRequest
425 from canonical.config import config
426+from canonical.launchpad.webapp.errorlog import ErrorReportEvent
427 from canonical.launchpad.webapp.interaction import ANONYMOUS
428 from canonical.launchpad.webapp.interfaces import ILaunchBag
429 from canonical.launchpad.windmill.testing import constants
430@@ -97,6 +98,7 @@
431 from lp.testing._tales import test_tales
432 from lp.testing._webservice import (
433 launchpadlib_credentials_for, launchpadlib_for, oauth_access_token_for)
434+from lp.testing.fixture import ZopeEventHandlerFixture
435
436 # zope.exception demands more of frame objects than twisted.python.failure
437 # provides in its fake frames. This is enough to make it work with them
438@@ -384,6 +386,14 @@
439 testtools.TestCase.setUp(self)
440 from lp.testing.factory import ObjectFactory
441 self.factory = ObjectFactory()
442+ # Record the oopses generated during the test run.
443+ self.oopses = []
444+ self.installFixture(ZopeEventHandlerFixture(self._recordOops))
445+
446+ @adapter(ErrorReportEvent)
447+ def _recordOops(self, event):
448+ """Add the oops to the testcase's list."""
449+ self.oopses.append(event.object)
450
451 def assertStatementCount(self, expected_count, function, *args, **kwargs):
452 """Assert that the expected number of SQL statements occurred.
453
454=== renamed file 'lib/lp/codehosting/scanner/fixture.py' => 'lib/lp/testing/fixture.py'
455=== renamed file 'lib/lp/codehosting/scanner/tests/test_fixture.py' => 'lib/lp/testing/tests/test_fixture.py'
456--- lib/lp/codehosting/scanner/tests/test_fixture.py 2009-06-30 16:56:07 +0000
457+++ lib/lp/testing/tests/test_fixture.py 2010-05-03 14:15:42 +0000
458@@ -9,7 +9,7 @@
459
460 from zope.interface import implements
461
462-from lp.codehosting.scanner.fixture import (
463+from lp.testing.fixture import (
464 Fixtures, FixtureWithCleanup, IFixture, run_with_fixture, with_fixture)
465 from lp.testing import TestCase
466

Subscribers

People subscribed via source and target branches

to status/vote changes: