Merge lp:~abentley/launchpad/celery-everywhere-9 into lp:launchpad

Proposed by Aaron Bentley on 2012-04-26
Status: Merged
Approved by: j.c.sackett on 2012-04-26
Approved revision: no longer in the source branch.
Merged at revision: 15175
Proposed branch: lp:~abentley/launchpad/celery-everywhere-9
Merge into: lp:launchpad
Prerequisite: lp:~abentley/launchpad/celery-everywhere-8
Diff against target: 713 lines (+228/-173)
5 files modified
lib/lp/answers/model/questionjob.py (+10/-2)
lib/lp/answers/tests/test_questionjob.py (+106/-117)
lib/lp/services/job/model/job.py (+4/-1)
lib/lp/soyuz/model/packagecopyjob.py (+14/-2)
lib/lp/soyuz/tests/test_packagecopyjob.py (+94/-51)
To merge this branch: bzr merge lp:~abentley/launchpad/celery-everywhere-9
Reviewer Review Type Date Requested Status
j.c.sackett (community) 2012-04-26 Approve on 2012-04-26
Richard Harding (community) code* 2012-04-26 Approve on 2012-04-26
Review via email: mp+103723@code.launchpad.net

Commit Message

Support running QuestionJob and PlainPackageCopyJob via Celery.

Description of the Change

= Summary =
Support running QuestionJob and PackageCopyJob via Celery.

== Pre-implementation notes ==
None

== LOC Rationale ==
Part of a resourced arc that will remove code.

== Implementation details ==
Clean up test set up for both sets of tests. Add makeDerived methods to support UniversalJobSource, and QuestionJob and PackageCopyJob support to UniversalJobSource.

Use EnumeratedSubclass as the metatype for PackageCopyDerived, to support makeDerived method.

Add config members to jobs to support UniversalJobSource, which uses this to determine database user.

Extract simplified test setup.

Add TestViaCelery test cases for both classes.

== Tests ==
bin/test -vm 'test_questionjob|test_packagecopyjob'

== Demo and Q/A ==
None

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/tests/test_membership_notification_job.py
  lib/lp/soyuz/model/packagecopyjob.py
  lib/lp/soyuz/tests/test_packagecopyjob.py
  lib/lp/answers/model/questionjob.py
  lib/lp/registry/model/persontransferjob.py
  lib/lp/registry/tests/test_person_merge_job.py
  lib/lp/services/job/model/job.py
  lib/lp/answers/tests/test_questionjob.py

To post a comment you must log in.
Richard Harding (rharding) wrote :

Thanks Aaron, couple questions, but looks good and ok'ing:

- I'm curious why you didn't make the make_question_job into a factory method and use it like the other factory createXXXX methods?

- I know it's just in testing, but with make_user_subject_body_headers it'd be great to have either a namedtuple or dictionary coming back. Keeping the right params in the right order is more likely to break.

- In the final test can you add a check for some bit of the email to make sure it's the one you're expecting.

review: Approve (code*)
Aaron Bentley (abentley) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 12-04-26 02:33 PM, Richard Harding wrote:
> Review: Approve code*
>
> Thanks Aaron, couple questions, but looks good and ok'ing:
>
> - I'm curious why you didn't make the make_question_job into a
> factory method and use it like the other factory createXXXX
> methods?

I wanted to use it with multiple classes. If it had been a method, I
wouldn't have been able to do that.

> - I know it's just in testing, but with
> make_user_subject_body_headers it'd be great to have either a
> namedtuple or dictionary coming back. Keeping the right params in
> the right order is more likely to break.

I've never encountered a namedtuple in Launchpad code, nor seen a
dictionary used that way. I don't we should disparage returning
multiple values; it's a key Python feature.

Also, I just moved that code around-- it already returned multiple values.

> - In the final test can you add a check for some bit of the email
> to make sure it's the one you're expecting.

There are thorough tests to ensure that the output email is correct.
The purpose of this test is to check the integration with Celery.
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk+ZqKYACgkQ0F+nu1YWqI1dPQCfaYoYCpfTKgoewKdqvFdBR7c4
+NsAoIUuxjBhTttgPEyqRq3sJ+Wigi9n
=JztV
-----END PGP SIGNATURE-----

j.c.sackett (jcsackett) wrote :

Aaron--

I'm guessing the rationale for make_question_job is that there's no need for it outside that file? As such I'm fine with not adding it to the factory.

Rick's point about named tuple is a good one, but I think it's very optional. I agree with his request for the additional check, though.

review: Approve
Aaron Bentley (abentley) wrote :

> I'm guessing the rationale for make_question_job is that there's no need for
> it outside that file? As such I'm fine with not adding it to the factory.

Right. It's not a generic function to create a QuestionJob, it's specifically for (some of) these tests.

> Rick's point about named tuple is a good one, but I think it's very optional.
> I agree with his request for the additional check, though.

Okay.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/answers/model/questionjob.py'
2--- lib/lp/answers/model/questionjob.py 2011-12-30 06:14:56 +0000
3+++ lib/lp/answers/model/questionjob.py 2012-04-27 15:01:30 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2011 Canonical Ltd. This software is licensed under the
6+# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Job classes related to QuestionJob."""
10@@ -94,6 +94,11 @@
11 """See `IQuestionJob`."""
12 return simplejson.loads(self._json_data)
13
14+ def makeDerived(self):
15+ if self.job_type != QuestionJobType.EMAIL:
16+ raise ValueError('Unsupported Job type')
17+ return QuestionEmailJob(self)
18+
19
20 class QuestionEmailJob(BaseRunnableJob):
21 """Intermediate class for deriving from QuestionJob."""
22@@ -101,6 +106,7 @@
23 delegates(IQuestionJob)
24 implements(IQuestionEmailJob)
25 classProvides(IQuestionEmailJobSource)
26+ config = config.IQuestionEmailJobSource
27
28 def __init__(self, job):
29 self.context = job
30@@ -119,7 +125,9 @@
31 }
32 job = QuestionJob(
33 question=question, job_type=cls.class_job_type, metadata=metadata)
34- return cls(job)
35+ derived = cls(job)
36+ derived.celeryRunOnCommit()
37+ return derived
38
39 @classmethod
40 def iterReady(cls):
41
42=== modified file 'lib/lp/answers/tests/test_questionjob.py'
43--- lib/lp/answers/tests/test_questionjob.py 2012-01-01 02:58:52 +0000
44+++ lib/lp/answers/tests/test_questionjob.py 2012-04-27 15:01:30 +0000
45@@ -26,7 +26,12 @@
46 QuestionJob,
47 )
48 from lp.services.database.lpstorm import IStore
49+from lp.services.features.testing import FeatureFixture
50 from lp.services.job.interfaces.job import JobStatus
51+from lp.services.job.tests import (
52+ block_on_job,
53+ pop_remote_notifications,
54+ )
55 from lp.services.log.logger import BufferLogger
56 from lp.services.mail import stub
57 from lp.services.mail.sendmail import (
58@@ -40,7 +45,10 @@
59 run_script,
60 TestCaseWithFactory,
61 )
62-from lp.testing.layers import DatabaseFunctionalLayer
63+from lp.testing.layers import (
64+ CeleryJobLayer,
65+ DatabaseFunctionalLayer,
66+ )
67
68
69 class QuestionJobTestCase(TestCaseWithFactory):
70@@ -70,31 +78,45 @@
71 repr(question_job))
72
73
74+def make_user_subject_body_headers(factory):
75+ user = factory.makePerson()
76+ subject = 'email subject'
77+ body = 'email body'
78+ headers = {'X-Launchpad-Question': 'question metadata'}
79+ return user, subject, body, headers
80+
81+
82+def make_question_job(factory, recipient_set=QuestionRecipientSet.SUBSCRIBER,
83+ body=None, question=None, user=None):
84+ if question is None:
85+ question = factory.makeQuestion()
86+ default_user, subject, default_body, headers = (
87+ make_user_subject_body_headers(factory))
88+ if body is None:
89+ body = default_body
90+ if user is None:
91+ user = default_user
92+ contact = factory.makePerson()
93+ with person_logged_in(contact):
94+ lang_set = getUtility(ILanguageSet)
95+ contact.addLanguage(lang_set['en'])
96+ question.target.addAnswerContact(contact, contact)
97+ return QuestionEmailJob.create(
98+ question, user, recipient_set,
99+ subject, body, headers)
100+
101+
102 class QuestionEmailJobTestCase(TestCaseWithFactory):
103 """Test case for QuestionEmailJob class."""
104
105 layer = DatabaseFunctionalLayer
106
107- def makeUserSubjectBodyHeaders(self):
108- user = self.factory.makePerson()
109- subject = 'email subject'
110- body = 'email body'
111- headers = {'X-Launchpad-Question': 'question metadata'}
112- return user, subject, body, headers
113-
114- def addAnswerContact(self, question):
115- contact = self.factory.makePerson()
116- with person_logged_in(contact):
117- lang_set = getUtility(ILanguageSet)
118- contact.addLanguage(lang_set['en'])
119- question.target.addAnswerContact(contact, contact)
120- return contact
121-
122 def test_create(self):
123 # The create class method converts the extra job arguments
124 # to metadata.
125 question = self.factory.makeQuestion()
126- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
127+ user, subject, body, headers = make_user_subject_body_headers(
128+ self.factory)
129 job = QuestionEmailJob.create(
130 question, user, QuestionRecipientSet.SUBSCRIBER,
131 subject, body, headers)
132@@ -124,16 +146,10 @@
133 title='title', description='description', owner=asker,
134 language=getUtility(ILanguageSet)['en'],
135 product=product, distribution=None, sourcepackagename=None)
136- user, subject, ignore, headers = self.makeUserSubjectBodyHeaders()
137- job_1 = QuestionEmailJob.create(
138- question, user, QuestionRecipientSet.SUBSCRIBER,
139- subject, 'one', headers)
140- job_2 = QuestionEmailJob.create(
141- question, user, QuestionRecipientSet.SUBSCRIBER,
142- subject, 'two', headers)
143- job_3 = QuestionEmailJob.create(
144- question, user, QuestionRecipientSet.SUBSCRIBER,
145- subject, 'three', headers)
146+ job_1 = make_question_job(self.factory, question=question, body='one')
147+ job_2 = make_question_job(self.factory, question=question, body='two')
148+ job_3 = make_question_job(
149+ self.factory, question=question, body='three')
150 job_1.start()
151 job_1.complete()
152 job_ids = [job.id for job in QuestionEmailJob.iterReady()]
153@@ -141,35 +157,27 @@
154
155 def test_user(self):
156 # The user property matches the user passed to create().
157- question = self.factory.makeQuestion()
158- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
159- job = QuestionEmailJob.create(
160- question, user, QuestionRecipientSet.SUBSCRIBER,
161- subject, body, headers)
162+ user = self.factory.makePerson()
163+ job = make_question_job(self.factory, user=user)
164 self.assertEqual(user, job.user)
165
166 def test_subject(self):
167 # The subject property matches the subject passed to create().
168- question = self.factory.makeQuestion()
169- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
170- job = QuestionEmailJob.create(
171- question, user, QuestionRecipientSet.SUBSCRIBER,
172- subject, body, headers)
173+ body = 'email body'
174+ job = make_question_job(self.factory, body=body)
175 self.assertEqual(body, job.body)
176
177 def test_body(self):
178 # The body property matches the body passed to create().
179- question = self.factory.makeQuestion()
180- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
181- job = QuestionEmailJob.create(
182- question, user, QuestionRecipientSet.SUBSCRIBER,
183- subject, body, headers)
184+ body = 'email body'
185+ job = make_question_job(self.factory, body=body)
186 self.assertEqual(body, job.body)
187
188 def test_headers(self):
189 # The headers property matches the headers passed to create().
190 question = self.factory.makeQuestion()
191- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
192+ user, subject, body, headers = make_user_subject_body_headers(
193+ self.factory)
194 job = QuestionEmailJob.create(
195 question, user, QuestionRecipientSet.SUBSCRIBER,
196 subject, body, headers)
197@@ -178,7 +186,8 @@
198 def test_from_address(self):
199 # The from_address is the question with the user displayname.
200 question = self.factory.makeQuestion()
201- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
202+ user, subject, body, headers = make_user_subject_body_headers(
203+ self.factory)
204 job = QuestionEmailJob.create(
205 question, user, QuestionRecipientSet.SUBSCRIBER,
206 subject, body, headers)
207@@ -189,17 +198,14 @@
208
209 def test_log_name(self):
210 # The log_name property matches the class name.
211- question = self.factory.makeQuestion()
212- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
213- job = QuestionEmailJob.create(
214- question, user, QuestionRecipientSet.SUBSCRIBER,
215- subject, body, headers)
216+ job = make_question_job(self.factory)
217 self.assertEqual(job.__class__.__name__, job.log_name)
218
219 def test_getOopsVars(self):
220 # The getOopsVars() method adds the question and user to the vars.
221 question = self.factory.makeQuestion()
222- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
223+ user, subject, body, headers = make_user_subject_body_headers(
224+ self.factory)
225 job = QuestionEmailJob.create(
226 question, user, QuestionRecipientSet.SUBSCRIBER,
227 subject, body, headers)
228@@ -209,104 +215,68 @@
229
230 def test_getErrorRecipients(self):
231 # The getErrorRecipients method matches the user.
232- question = self.factory.makeQuestion()
233- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
234- job = QuestionEmailJob.create(
235- question, user, QuestionRecipientSet.SUBSCRIBER,
236- subject, body, headers)
237+ job = make_question_job(self.factory)
238 self.assertEqual(
239 [format_address_for_person(job.user)], job.getErrorRecipients())
240
241 def test_recipients_asker(self):
242 # The recipients property contains the question owner.
243- question = self.factory.makeQuestion()
244- self.addAnswerContact(question)
245- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
246- job = QuestionEmailJob.create(
247- question, user, QuestionRecipientSet.ASKER,
248- subject, body, headers)
249+ job = make_question_job(self.factory, QuestionRecipientSet.ASKER)
250 recipients = [
251 person for email, person in job.recipients.getRecipientPersons()]
252 self.assertEqual(1, len(recipients))
253- self.assertEqual(question.owner, recipients[0])
254+ self.assertEqual(job.question.owner, recipients[0])
255
256 def test_recipients_subscriber(self):
257 # The recipients property matches the question recipients,
258 # excluding the question owner.
259- question = self.factory.makeQuestion()
260- self.addAnswerContact(question)
261- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
262- job = QuestionEmailJob.create(
263- question, user, QuestionRecipientSet.SUBSCRIBER,
264- subject, body, headers)
265+ job = make_question_job(self.factory)
266 recipients = [
267 person for email, person in job.recipients.getRecipientPersons()]
268- self.assertFalse(question.owner in recipients)
269+ self.assertFalse(job.question.owner in recipients)
270 question_recipients = [
271- person
272- for em, person in question.getRecipients().getRecipientPersons()
273- if person != question.owner]
274+ person for em, person in
275+ job.question.getRecipients().getRecipientPersons()
276+ if person != job.question.owner]
277 self.assertContentEqual(
278 question_recipients, recipients)
279
280 def test_recipients_asker_subscriber(self):
281 # The recipients property matches the question recipients.
282- question = self.factory.makeQuestion()
283- self.addAnswerContact(question)
284- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
285- job = QuestionEmailJob.create(
286- question, user, QuestionRecipientSet.ASKER_SUBSCRIBER,
287- subject, body, headers)
288+ job = make_question_job(
289+ self.factory, QuestionRecipientSet.ASKER_SUBSCRIBER)
290 self.assertContentEqual(
291- question.getRecipients().getRecipientPersons(),
292+ job.question.getRecipients().getRecipientPersons(),
293 job.recipients.getRecipientPersons())
294
295 def test_recipients_contact(self):
296 # The recipients property matches the question target answer contacts.
297- question = self.factory.makeQuestion()
298- self.addAnswerContact(question)
299- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
300- job = QuestionEmailJob.create(
301- question, user, QuestionRecipientSet.CONTACT,
302- subject, body, headers)
303+ job = make_question_job(
304+ self.factory, QuestionRecipientSet.CONTACT)
305 recipients = [
306 person for email, person in job.recipients.getRecipientPersons()]
307 self.assertContentEqual(
308- question.target.getAnswerContactRecipients(None),
309+ job.question.target.getAnswerContactRecipients(None),
310 recipients)
311
312 def test_buildBody_with_separator(self):
313 # A body with a separator is preserved.
314- question = self.factory.makeQuestion()
315- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
316- body = 'body\n-- '
317- job = QuestionEmailJob.create(
318- question, user, QuestionRecipientSet.SUBSCRIBER,
319- subject, body, headers)
320+ job = make_question_job(self.factory, body='body\n-- ')
321 formatted_body = job.buildBody('rationale')
322 self.assertEqual(
323 'body\n-- \nrationale', formatted_body)
324
325 def test_buildBody_without_separator(self):
326 # A separator will added to body if one is not present.
327- question = self.factory.makeQuestion()
328- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
329- body = 'body -- mdash'
330- job = QuestionEmailJob.create(
331- question, user, QuestionRecipientSet.SUBSCRIBER,
332- subject, body, headers)
333+ job = make_question_job(self.factory, body='body -- mdash')
334 formatted_body = job.buildBody('rationale')
335 self.assertEqual(
336 'body -- mdash\n-- \nrationale', formatted_body)
337
338 def test_buildBody_wrapping(self):
339 # The rationale is wrapped and added to the body.
340- question = self.factory.makeQuestion()
341- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
342 body = 'body\n-- '
343- job = QuestionEmailJob.create(
344- question, user, QuestionRecipientSet.SUBSCRIBER,
345- subject, body, headers)
346+ job = make_question_job(self.factory, body=body)
347 rationale_1 = (
348 'You received this email because you are indirectly subscribed '
349 'to this')
350@@ -319,42 +289,36 @@
351
352 def test_run(self):
353 # The email is sent to all the recipients.
354- question = self.factory.makeQuestion()
355- self.addAnswerContact(question)
356- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
357- job = QuestionEmailJob.create(
358- question, user, QuestionRecipientSet.ASKER_SUBSCRIBER,
359- subject, body, headers)
360+ job = make_question_job(
361+ self.factory, QuestionRecipientSet.ASKER_SUBSCRIBER)
362 logger = BufferLogger()
363 with log.use(logger):
364 job.run()
365 self.assertEqual(
366 ["DEBUG QuestionEmailJob will send email for question %s." %
367- question.id,
368+ job.question.id,
369 "DEBUG QuestionEmailJob has sent email for question %s." %
370- question.id],
371+ job.question.id],
372 logger.getLogBuffer().splitlines())
373 transaction.commit()
374 self.assertEqual(2, len(stub.test_emails))
375
376 def test_run_cronscript(self):
377 # The cronscript is configured: schema-lazr.conf and security.cfg.
378- question = self.factory.makeQuestion()
379+ job = make_question_job(
380+ self.factory, QuestionRecipientSet.ASKER_SUBSCRIBER)
381+ question = job.question
382 with person_logged_in(question.target.owner):
383 question.linkBug(self.factory.makeBug(product=question.target))
384 question.linkFAQ(
385 question.target.owner,
386 self.factory.makeFAQ(target=question.target),
387 'test FAQ link')
388- self.addAnswerContact(question)
389- user, subject, body, headers = self.makeUserSubjectBodyHeaders()
390+ user = job.user
391 with person_logged_in(user):
392 lang_set = getUtility(ILanguageSet)
393 user.addLanguage(lang_set['en'])
394 question.target.addAnswerContact(user, user)
395- job = QuestionEmailJob.create(
396- question, user, QuestionRecipientSet.ASKER_SUBSCRIBER,
397- subject, body, headers)
398 transaction.commit()
399
400 out, err, exit_code = run_script(
401@@ -372,3 +336,28 @@
402 'Cound not find "%s" in err log:\n%s.' % (message, err))
403 IStore(job.job).invalidate()
404 self.assertEqual(JobStatus.COMPLETED, job.job.status)
405+
406+
407+class TestViaCelery(TestCaseWithFactory):
408+
409+ layer = CeleryJobLayer
410+
411+ def test_run(self):
412+ # The email is sent to all the recipients.
413+ # Create the question before turning on the feature flag to avoid
414+ # running two jobs via Celery.
415+ question = self.factory.makeQuestion()
416+ self.useFixture(FeatureFixture({
417+ 'jobs.celery.enabled_classes': 'QuestionEmailJob',
418+ }))
419+ body = self.factory.getUniqueString('body')
420+ make_question_job(
421+ self.factory, QuestionRecipientSet.ASKER_SUBSCRIBER,
422+ question=question, body=body)
423+ with block_on_job(self):
424+ transaction.commit()
425+ transaction.commit()
426+ mail = pop_remote_notifications()
427+ self.assertEqual(2, len(mail))
428+ for message in mail:
429+ self.assertIn(body, message.get_payload())
430
431=== modified file 'lib/lp/services/job/model/job.py'
432--- lib/lp/services/job/model/job.py 2012-04-25 15:35:48 +0000
433+++ lib/lp/services/job/model/job.py 2012-04-27 15:01:30 +0000
434@@ -277,7 +277,9 @@
435 BranchMergeProposalJob,
436 )
437 from lp.registry.model.persontransferjob import PersonTransferJob
438+ from lp.answers.model.questionjob import QuestionJob
439 from lp.soyuz.model.distributionjob import DistributionJob
440+ from lp.soyuz.model.packagecopyjob import PackageCopyJob
441 from lp.translations.model.pofilestatsjob import POFileStatsJob
442 from lp.translations.model.translationsharingjob import (
443 TranslationSharingJob,
444@@ -287,7 +289,8 @@
445
446 for baseclass in [
447 ApportJob, BranchJob, BranchMergeProposalJob, DistributionJob,
448- PersonTransferJob, POFileStatsJob, TranslationSharingJob,
449+ PackageCopyJob, PersonTransferJob, POFileStatsJob, QuestionJob,
450+ TranslationSharingJob,
451 ]:
452 derived, base_class, store = cls._getDerived(job_id, baseclass)
453 if derived is not None:
454
455=== modified file 'lib/lp/soyuz/model/packagecopyjob.py'
456--- lib/lp/soyuz/model/packagecopyjob.py 2012-03-16 11:09:03 +0000
457+++ lib/lp/soyuz/model/packagecopyjob.py 2012-04-27 15:01:30 +0000
458@@ -40,6 +40,7 @@
459 from lp.registry.interfaces.pocket import PackagePublishingPocket
460 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
461 from lp.registry.model.distroseries import DistroSeries
462+from lp.services.config import config
463 from lp.services.database import bulk
464 from lp.services.database.decoratedresultset import DecoratedResultSet
465 from lp.services.database.enumcol import EnumCol
466@@ -51,7 +52,10 @@
467 from lp.services.job.interfaces.job import (
468 JobStatus,
469 )
470-from lp.services.job.model.job import Job
471+from lp.services.job.model.job import (
472+ EnumeratedSubclass,
473+ Job,
474+ )
475 from lp.services.job.runner import BaseRunnableJob
476 from lp.soyuz.adapters.overrides import (
477 FromExistingOverridePolicy,
478@@ -170,10 +174,15 @@
479 """See `IPackageCopyJob`."""
480 return self.metadata.get("section_override")
481
482+ def makeDerived(self):
483+ return PackageCopyJobDerived.makeSubclass(self)
484+
485
486 class PackageCopyJobDerived(BaseRunnableJob):
487 """Abstract class for deriving from PackageCopyJob."""
488
489+ __metaclass__ = EnumeratedSubclass
490+
491 delegates(IPackageCopyJob)
492
493 def __init__(self, job):
494@@ -235,6 +244,7 @@
495
496 class_job_type = PackageCopyJobType.PLAIN
497 classProvides(IPlainPackageCopyJobSource)
498+ config = config.IPlainPackageCopyJobSource
499
500 @classmethod
501 def _makeMetadata(cls, target_pocket, package_version,
502@@ -272,7 +282,9 @@
503 metadata=metadata,
504 requester=requester)
505 IMasterStore(PackageCopyJob).add(job)
506- return cls(job)
507+ derived = cls(job)
508+ derived.celeryRunOnCommit()
509+ return derived
510
511 @classmethod
512 def _composeJobInsertionTuple(cls, target_distroseries, copy_policy,
513
514=== modified file 'lib/lp/soyuz/tests/test_packagecopyjob.py'
515--- lib/lp/soyuz/tests/test_packagecopyjob.py 2012-03-16 11:09:03 +0000
516+++ lib/lp/soyuz/tests/test_packagecopyjob.py 2012-04-27 15:01:30 +0000
517@@ -1,4 +1,4 @@
518-# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
519+# Copyright 2010-2012 Canonical Ltd. This software is licensed under the
520 # GNU Affero General Public License version 3 (see the file LICENSE).
521
522 """Tests for sync package jobs."""
523@@ -27,6 +27,10 @@
524 from lp.services.job.interfaces.job import (
525 JobStatus,
526 )
527+from lp.services.job.tests import (
528+ block_on_job,
529+ pop_remote_notifications,
530+ )
531 from lp.services.webapp.testing import verifyObject
532 from lp.soyuz.adapters.overrides import SourceOverride
533 from lp.soyuz.enums import (
534@@ -56,6 +60,7 @@
535 from lp.soyuz.model.queue import PackageUpload
536 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
537 from lp.testing import (
538+ celebrity_logged_in,
539 person_logged_in,
540 run_script,
541 TestCaseWithFactory,
542@@ -63,6 +68,7 @@
543 from lp.testing.dbuser import switch_dbuser
544 from lp.testing.fakemethod import FakeMethod
545 from lp.testing.layers import (
546+ CeleryJobLayer,
547 LaunchpadFunctionalLayer,
548 LaunchpadZopelessLayer,
549 ZopelessDatabaseLayer,
550@@ -78,6 +84,48 @@
551 DistroSeriesDifferenceComment.distro_series_difference == dsd)
552
553
554+def create_proper_job(factory):
555+ """Create a job that will complete successfully."""
556+ publisher = SoyuzTestPublisher()
557+ with celebrity_logged_in('admin'):
558+ publisher.prepareBreezyAutotest()
559+ distroseries = publisher.breezy_autotest
560+
561+ # Synchronise from breezy-autotest to a brand new distro derived
562+ # from breezy.
563+ breezy_archive = factory.makeArchive(
564+ distroseries.distribution, purpose=ArchivePurpose.PRIMARY)
565+ dsp = factory.makeDistroSeriesParent(parent_series=distroseries)
566+ target_series = dsp.derived_series
567+ target_archive = factory.makeArchive(
568+ target_series.distribution, purpose=ArchivePurpose.PRIMARY)
569+ getUtility(ISourcePackageFormatSelectionSet).add(
570+ target_series, SourcePackageFormat.FORMAT_1_0)
571+
572+ publisher.getPubSource(
573+ distroseries=distroseries, sourcename="libc",
574+ version="2.8-1", status=PackagePublishingStatus.PUBLISHED,
575+ archive=breezy_archive)
576+ # The target archive needs ancestry so the package is
577+ # auto-accepted.
578+ publisher.getPubSource(
579+ distroseries=target_series, sourcename="libc",
580+ version="2.8-0", status=PackagePublishingStatus.PUBLISHED,
581+ archive=target_archive)
582+
583+ source = getUtility(IPlainPackageCopyJobSource)
584+ requester = factory.makePerson()
585+ with person_logged_in(target_archive.owner):
586+ target_archive.newComponentUploader(requester, "main")
587+ return source.create(
588+ package_name="libc",
589+ source_archive=breezy_archive, target_archive=target_archive,
590+ target_distroseries=target_series,
591+ target_pocket=PackagePublishingPocket.RELEASE,
592+ package_version="2.8-1", include_binaries=False,
593+ requester=requester)
594+
595+
596 class LocalTestHelper:
597 """Put test helpers that want to be in the test classes here."""
598
599@@ -355,62 +403,26 @@
600 # Turn on DSD jobs.
601 self.useFixture(FeatureFixture({FEATURE_FLAG_ENABLE_MODULE: 'on'}))
602
603- publisher = SoyuzTestPublisher()
604- publisher.prepareBreezyAutotest()
605- distroseries = publisher.breezy_autotest
606-
607- # Synchronise from breezy-autotest to a brand new distro derived
608- # from breezy.
609- breezy_archive = self.factory.makeArchive(
610- distroseries.distribution, purpose=ArchivePurpose.PRIMARY)
611- dsp = self.factory.makeDistroSeriesParent(parent_series=distroseries)
612- target_series = dsp.derived_series
613- target_archive = self.factory.makeArchive(
614- target_series.distribution, purpose=ArchivePurpose.PRIMARY)
615- getUtility(ISourcePackageFormatSelectionSet).add(
616- target_series, SourcePackageFormat.FORMAT_1_0)
617-
618- publisher.getPubSource(
619- distroseries=distroseries, sourcename="libc",
620- version="2.8-1", status=PackagePublishingStatus.PUBLISHED,
621- archive=breezy_archive)
622- # The target archive needs ancestry so the package is
623- # auto-accepted.
624- publisher.getPubSource(
625- distroseries=target_series, sourcename="libc",
626- version="2.8-0", status=PackagePublishingStatus.PUBLISHED,
627- archive=target_archive)
628-
629- source = getUtility(IPlainPackageCopyJobSource)
630- requester = self.factory.makePerson()
631- with person_logged_in(target_archive.owner):
632- target_archive.newComponentUploader(requester, "main")
633- job = source.create(
634- package_name="libc",
635- source_archive=breezy_archive, target_archive=target_archive,
636- target_distroseries=target_series,
637- target_pocket=PackagePublishingPocket.RELEASE,
638- package_version="2.8-1", include_binaries=False,
639- requester=requester)
640+ job = create_proper_job(self.factory)
641 self.assertEqual("libc", job.package_name)
642 self.assertEqual("2.8-1", job.package_version)
643
644 switch_dbuser(self.dbuser)
645- job.run()
646-
647- published_sources = target_archive.getPublishedSources(
648- name="libc", version="2.8-1")
649- self.assertIsNot(None, published_sources.any())
650-
651- # The copy should have sent an email too. (see
652- # soyuz/scripts/tests/test_copypackage.py for detailed
653- # notification tests)
654- emails = pop_notifications()
655- self.assertTrue(len(emails) > 0)
656-
657 # Switch back to a db user that has permission to clean up
658 # featureflag.
659- switch_dbuser('launchpad_main')
660+ self.addCleanup(switch_dbuser, 'launchpad_main')
661+ pop_notifications()
662+ job.run()
663+
664+ published_sources = job.target_archive.getPublishedSources(
665+ name="libc", version="2.8-1")
666+ self.assertIsNot(None, published_sources.any())
667+
668+ # The copy should have sent an email too. (see
669+ # soyuz/scripts/tests/test_copypackage.py for detailed
670+ # notification tests)
671+ emails = pop_notifications()
672+ self.assertEqual(len(emails), 1)
673
674 def test_getOopsVars(self):
675 distroseries = self.factory.makeDistroSeries()
676@@ -1234,6 +1246,37 @@
677 self.assertEqual(PackageUploadStatus.REJECTED, pu.status)
678
679
680+class TestViaCelery(TestCaseWithFactory):
681+ """PackageCopyJob runs under Celery."""
682+
683+ layer = CeleryJobLayer
684+
685+ def test_run(self):
686+ # A proper test run synchronizes packages.
687+ # Turn on DSD jobs.
688+ self.useFixture(FeatureFixture({
689+ FEATURE_FLAG_ENABLE_MODULE: 'on',
690+ 'jobs.celery.enabled_classes': 'PlainPackageCopyJob',
691+ }))
692+
693+ job = create_proper_job(self.factory)
694+ self.assertEqual("libc", job.package_name)
695+ self.assertEqual("2.8-1", job.package_version)
696+
697+ with block_on_job(self):
698+ transaction.commit()
699+
700+ published_sources = job.target_archive.getPublishedSources(
701+ name="libc", version="2.8-1")
702+ self.assertIsNot(None, published_sources.any())
703+
704+ # The copy should have sent an email too. (see
705+ # soyuz/scripts/tests/test_copypackage.py for detailed
706+ # notification tests)
707+ emails = pop_remote_notifications()
708+ self.assertEqual(1, len(emails))
709+
710+
711 class TestPlainPackageCopyJobPermissions(TestCaseWithFactory):
712
713 layer = LaunchpadFunctionalLayer