Merge lp:~jtv/launchpad/bug-730460-job-class into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 12588
Proposed branch: lp:~jtv/launchpad/bug-730460-job-class
Merge into: lp:launchpad
Diff against target: 524 lines (+342/-8)
12 files modified
database/schema/security.cfg (+3/-0)
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+5/-1)
lib/lp/registry/interfaces/distroseries.py (+6/-1)
lib/lp/registry/model/distroseries.py (+8/-3)
lib/lp/registry/tests/test_distroseries.py (+7/-1)
lib/lp/soyuz/configure.zcml (+10/-0)
lib/lp/soyuz/interfaces/distributionjob.py (+7/-0)
lib/lp/soyuz/interfaces/distroseriesdifferencejob.py (+24/-0)
lib/lp/soyuz/model/distributionjob.py (+6/-2)
lib/lp/soyuz/model/distroseriesdifferencejob.py (+113/-0)
lib/lp/soyuz/model/publishing.py (+7/-0)
lib/lp/soyuz/tests/test_distroseriesdifferencejob.py (+146/-0)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-730460-job-class
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+52857@code.launchpad.net

Commit message

[r=allenap][bug=730460] DistroSeriesDifferenceJob.

Description of the change

= Summary =

Create a job class DistroSeriesDifferenceJob, and create jobs as needed as source package releases are published.

== Proposed fix ==

It's pathetic, really. I based my new job class on DistributionJob, which isn't really all that similar. The sad, sad fact is that the current job system encourages us to design "class hierarchies" based on what columns a type of job happens to need.

Before I land this branch I may want to implement a feature flag in a follow-up branch, so we don't create lots of DistroSeriesDifferenceJobs too early on. But it'd be nice to have this branch available for integration with other ongoing work.

== Pre-implementation notes ==

Discussed repeatedly with bigjools and StevenK. Using DistributionJob as a base was found to be a relatively practical tradeoff.

Also discussed: the guard code against redundant jobs is fragile, since it relies on a textually exact reproduction of a simple JSON data structure. This wasn't considered a big problem as long as redundant jobs don't dominate the queue.

== Implementation details ==

All fairly straightforward. Surprisingly DistroSeries offered no way to find derived series, so I had to add one.

You'll note that the new job type's "run" method does nothing. StevenK is working on an implementation, but that's a separate task. His branch will probably depend on mine.

== Tests ==

{{{
./bin/test -vvc lp.soyuz.tests.test_distroseriesdifferencejob
./bin/test -vvc lp.registry.tests.test_distroseries
}}}

== Demo and Q/A ==

Source-package publishing must still work, and the appropriate DistroSeriesDifferenceJobs will be created as a side effect.

= Launchpad lint =

I fixed most, but left a few reasonable-looking bits in place:

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/soyuz/interfaces/distroseriesdifferencejob.py
  lib/lp/soyuz/model/distroseriesdifferencejob.py
  lib/lp/soyuz/model/distributionjob.py
  lib/lp/registry/tests/test_distroseries.py
  lib/lp/soyuz/model/publishing.py
  lib/lp/registry/interfaces/distroseries.py
  lib/lp/soyuz/configure.zcml
  lib/lp/soyuz/interfaces/distributionjob.py
  lib/lp/soyuz/tests/test_distroseriesdifferencejob.py
  lib/lp/registry/model/distroseries.py
  lib/canonical/launchpad/interfaces/_schema_circular_imports.py

./lib/lp/registry/interfaces/distroseries.py
     430: E301 expected 1 blank line, found 2
     471: E301 expected 1 blank line, found 0
./lib/lp/registry/model/distroseries.py
     403: E301 expected 1 blank line, found 2

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Nice, +1.

[1]

+ child_series = distroseries.getDerivedSeries()
+ parent_series = distroseries.parent_series

Consider calling the variable child_serieses. Even though it's
probably not correct English, it helps to differentiate it from
parent_series which is a single DistroSeries instance.

[2]

+ layer = ZopelessDatabaseLayer

I always get confused by this. "Zopeless" actually means "component
architecture" doesn't it? That makes little sense to me, so I keep
forgetting it. What is your understanding? Another's perspective might
help me remember.

[3]

+ def test_make_metadata_is_consistent(self):
+ package = self.factory.makeSourcePackageName()
+ self.assertEqual(make_metadata(package), make_metadata(package))

If someone adds anything to make_metadata() this is going to become
fragile, unless the JSON lib sorts dictionary keys. Mmm, we shall
see. Good call adding this.

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Thanks. To satisfy both clarity and grammar, I renamed "child_series" to "children" (and to satisfy consistency as well, "parent_series" to "parent").

About Zopeless: yes, it means "component architecture only" and in fact I've had it recommended to me as something to use completely independently of Zope. So it seems to be sort of a "doesn't count" thing.

I hope we're simply not going to add any data to the new job type. The design deliberately does not try to include versions etc. because they may have changed by the time the job gets processed. Better for the job runner to figure it all out for itself.

Jeroen

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2011-03-10 07:53:15 +0000
3+++ database/schema/security.cfg 2011-03-11 07:06:49 +0000
4@@ -845,6 +845,7 @@
5 public.archivepermission = SELECT, INSERT
6 public.archivesubscriber = SELECT, UPDATE
7 public.binarypackagepublishinghistory = SELECT
8+public.distributionjob = SELECT, INSERT
9 public.gpgkey = SELECT, INSERT, UPDATE
10 public.packagecopyrequest = SELECT, INSERT, UPDATE
11 public.packagediff = SELECT, INSERT, UPDATE
12@@ -1258,6 +1259,7 @@
13 public.gpgkey = SELECT, INSERT
14 public.signedcodeofconduct = SELECT
15 public.distribution = SELECT, UPDATE
16+public.distributionjob = SELECT, INSERT
17 public.distroseries = SELECT, UPDATE
18 public.distroarchseries = SELECT
19 public.sourcepackagepublishinghistory = SELECT
20@@ -1357,6 +1359,7 @@
21 groups=script
22 # Announce handling
23 public.account = SELECT, INSERT
24+public.distributionjob = SELECT, INSERT
25 public.person = SELECT, INSERT
26 public.personsettings = SELECT, INSERT
27 public.emailaddress = SELECT, INSERT
28
29=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
30--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-03-03 05:12:33 +0000
31+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-03-11 07:06:49 +0000
32@@ -405,6 +405,9 @@
33 patch_reference_property(IDistroSeries, 'parent_series', IDistroSeries)
34 patch_plain_parameter_type(
35 IDistroSeries, 'deriveDistroSeries', 'distribution', IDistribution)
36+patch_collection_return_type(
37+ IDistroSeries, 'getDerivedSeries', IDistroSeries)
38+
39
40 # IDistroSeriesDifferenceComment
41 IDistroSeriesDifferenceComment['comment_author'].schema = IPerson
42@@ -552,7 +555,8 @@
43
44 # ISpecification
45 patch_collection_property(ISpecification, 'dependencies', ISpecification)
46-patch_collection_property(ISpecification, 'linked_branches', ISpecificationBranch)
47+patch_collection_property(
48+ ISpecification, 'linked_branches', ISpecificationBranch)
49
50 # ISpecificationTarget
51 patch_entry_return_type(
52
53=== modified file 'lib/lp/registry/interfaces/distroseries.py'
54--- lib/lp/registry/interfaces/distroseries.py 2011-03-04 04:28:07 +0000
55+++ lib/lp/registry/interfaces/distroseries.py 2011-03-11 07:06:49 +0000
56@@ -1,4 +1,4 @@
57-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
58+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
59 # GNU Affero General Public License version 3 (see the file LICENSE).
60
61 # pylint: disable-msg=E0211,E0213
62@@ -863,6 +863,11 @@
63 will be.
64 """
65
66+ @operation_returns_collection_of(Interface)
67+ @export_read_operation()
68+ def getDerivedSeries():
69+ """Get all `DistroSeries` derived from this one."""
70+
71
72 class IDistroSeries(IDistroSeriesEditRestricted, IDistroSeriesPublic,
73 IStructuralSubscriptionTarget):
74
75=== modified file 'lib/lp/registry/model/distroseries.py'
76--- lib/lp/registry/model/distroseries.py 2011-03-08 04:43:36 +0000
77+++ lib/lp/registry/model/distroseries.py 2011-03-11 07:06:49 +0000
78@@ -1,4 +1,4 @@
79-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
80+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
81 # GNU Affero General Public License version 3 (see the file LICENSE).
82
83 # pylint: disable-msg=E0611,W0212
84@@ -739,10 +739,10 @@
85 @property
86 def bugtargetname(self):
87 """See IBugTarget."""
88- return self.fullseriesname
89 # XXX mpt 2007-07-10 bugs 113258, 113262:
90 # The distribution's and series' names should be used instead
91 # of fullseriesname.
92+ return self.fullseriesname
93
94 @property
95 def bugtargetdisplayname(self):
96@@ -984,7 +984,7 @@
97 def getCurrentSourceReleases(self, source_package_names):
98 """See `IDistroSeries`."""
99 return getUtility(IDistroSeriesSet).getCurrentSourceReleases(
100- {self:source_package_names})
101+ {self: source_package_names})
102
103 def getTranslatableSourcePackages(self):
104 """See `IDistroSeries`."""
105@@ -1928,6 +1928,11 @@
106 getUtility(IInitialiseDistroSeriesJobSource).create(
107 child, architectures, packagesets, rebuild)
108
109+ def getDerivedSeries(self):
110+ """See `IDistroSeriesPublic`."""
111+ return Store.of(self).find(
112+ DistroSeries, DistroSeries.parent_series == self)
113+
114
115 class DistroSeriesSet:
116 implements(IDistroSeriesSet)
117
118=== modified file 'lib/lp/registry/tests/test_distroseries.py'
119--- lib/lp/registry/tests/test_distroseries.py 2010-10-26 15:47:24 +0000
120+++ lib/lp/registry/tests/test_distroseries.py 2011-03-11 07:06:49 +0000
121@@ -1,4 +1,4 @@
122-# Copyright 2009 Canonical Ltd. This software is licensed under the
123+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
124 # GNU Affero General Public License version 3 (see the file LICENSE).
125
126 """Tests for distroseries."""
127@@ -205,6 +205,12 @@
128 distroseries.getDistroArchSeriesByProcessor(
129 processorfamily.processors[0]))
130
131+ def test_getDerivedSeries(self):
132+ distroseries = self.factory.makeDistroSeries(
133+ parent_series=self.factory.makeDistroSeries())
134+ self.assertContentEqual(
135+ [distroseries], distroseries.parent_series.getDerivedSeries())
136+
137
138 class TestDistroSeriesPackaging(TestCaseWithFactory):
139
140
141=== modified file 'lib/lp/soyuz/configure.zcml'
142--- lib/lp/soyuz/configure.zcml 2010-12-24 02:22:11 +0000
143+++ lib/lp/soyuz/configure.zcml 2011-03-11 07:06:49 +0000
144@@ -900,6 +900,16 @@
145 <allow interface="lp.soyuz.interfaces.distributionjob.IDistributionJob" />
146 </class>
147
148+ <!-- DistroSeriesDifferenceJobSource -->
149+ <class class="lp.soyuz.model.distroseriesdifferencejob.DistroSeriesDifferenceJob">
150+ <allow interface="lp.soyuz.interfaces.distributionjob.IDistributionJob" />
151+ </class>
152+ <securedutility
153+ component="lp.soyuz.model.distroseriesdifferencejob.DistroSeriesDifferenceJob"
154+ provides="lp.soyuz.interfaces.distroseriesdifferencejob.IDistroSeriesDifferenceJobSource">
155+ <allow interface="lp.soyuz.interfaces.distroseriesdifferencejob.IDistroSeriesDifferenceJobSource"/>
156+ </securedutility>
157+
158 <!-- SyncPackageJobSource -->
159 <securedutility
160 component="lp.soyuz.model.syncpackagejob.SyncPackageJob"
161
162=== modified file 'lib/lp/soyuz/interfaces/distributionjob.py'
163--- lib/lp/soyuz/interfaces/distributionjob.py 2010-12-02 16:13:51 +0000
164+++ lib/lp/soyuz/interfaces/distributionjob.py 2011-03-11 07:06:49 +0000
165@@ -76,6 +76,13 @@
166 This job copies a single package, optionally including binaries.
167 """)
168
169+ DISTROSERIESDIFFERENCE = DBItem(3, """
170+ Create, delete, or update a Distro Series Difference.
171+
172+ Updates the status of a potential difference between a derived
173+ distribution release series and its parent series.
174+ """)
175+
176
177 class IInitialiseDistroSeriesJobSource(IJobSource):
178 """An interface for acquiring IInitialiseDistroSeriesJobs."""
179
180=== added file 'lib/lp/soyuz/interfaces/distroseriesdifferencejob.py'
181--- lib/lp/soyuz/interfaces/distroseriesdifferencejob.py 1970-01-01 00:00:00 +0000
182+++ lib/lp/soyuz/interfaces/distroseriesdifferencejob.py 2011-03-11 07:06:49 +0000
183@@ -0,0 +1,24 @@
184+# Copyright 2011 Canonical Ltd. This software is licensed under the
185+# GNU Affero General Public License version 3 (see the file LICENSE).
186+
187+"""`IDistroSeriesDifferenceJob`."""
188+
189+__metaclass__ = type
190+__all__ = [
191+ 'IDistroSeriesDifferenceJobSource',
192+ ]
193+
194+from lp.services.job.interfaces.job import IJobSource
195+
196+
197+class IDistroSeriesDifferenceJobSource(IJobSource):
198+ """An `IJob` for creating `DistroSeriesDifference`s."""
199+
200+ def createForPackagePublication(distroseries, sourcepackagename):
201+ """Create jobs as appropriate for a given status publication.
202+
203+ :param distroseries: A `DistroSeries` that is assumed to be
204+ derived from another one.
205+ :param sourcepackagename: A `SourcePackageName` that is being
206+ published in `distroseries`.
207+ """
208
209=== modified file 'lib/lp/soyuz/model/distributionjob.py'
210--- lib/lp/soyuz/model/distributionjob.py 2011-01-20 19:39:08 +0000
211+++ lib/lp/soyuz/model/distributionjob.py 2011-03-11 07:06:49 +0000
212@@ -55,12 +55,16 @@
213
214 def __init__(self, distribution, distroseries, job_type, metadata):
215 super(DistributionJob, self).__init__()
216- json_data = simplejson.dumps(metadata)
217 self.job = Job()
218 self.distribution = distribution
219 self.distroseries = distroseries
220 self.job_type = job_type
221- self._json_data = json_data.decode('utf-8')
222+ self._json_data = self.serializeMetadata(metadata)
223+
224+ @classmethod
225+ def serializeMetadata(cls, metadata_dict):
226+ """Serialize a dict of metadata into a unicode string."""
227+ return simplejson.dumps(metadata_dict).decode('utf-8')
228
229 @property
230 def metadata(self):
231
232=== added file 'lib/lp/soyuz/model/distroseriesdifferencejob.py'
233--- lib/lp/soyuz/model/distroseriesdifferencejob.py 1970-01-01 00:00:00 +0000
234+++ lib/lp/soyuz/model/distroseriesdifferencejob.py 2011-03-11 07:06:49 +0000
235@@ -0,0 +1,113 @@
236+# Copyright 2011 Canonical Ltd. This software is licensed under the
237+# GNU Affero General Public License version 3 (see the file LICENSE).
238+
239+"""Job class to request generation or update of `DistroSeriesDifference`s."""
240+
241+__metaclass__ = type
242+__all__ = [
243+ 'DistroSeriesDifferenceJob',
244+ ]
245+
246+from zope.interface import (
247+ classProvides,
248+ implements,
249+ )
250+
251+from canonical.launchpad.interfaces.lpstorm import IMasterStore
252+from lp.services.job.model.job import Job
253+from lp.soyuz.interfaces.distributionjob import (
254+ DistributionJobType,
255+ IDistributionJob,
256+ )
257+from lp.soyuz.model.distributionjob import (
258+ DistributionJob,
259+ DistributionJobDerived,
260+ )
261+from lp.soyuz.interfaces.distroseriesdifferencejob import (
262+ IDistroSeriesDifferenceJobSource,
263+ )
264+
265+
266+def make_metadata(sourcepackagename):
267+ """Return JSON metadata for a job on `sourcepackagename`."""
268+ return {'sourcepackagename': sourcepackagename.id}
269+
270+
271+def create_job(distroseries, sourcepackagename):
272+ """Create a `DistroSeriesDifferenceJob` for a given source package.
273+
274+ :param distroseries: A `DistroSeries` that is assumed to be derived
275+ from another one.
276+ :param sourcepackagename: The `SourcePackageName` whose publication
277+ history has changed.
278+ """
279+ job = DistributionJob(
280+ distribution=distroseries.distribution, distroseries=distroseries,
281+ job_type=DistributionJobType.DISTROSERIESDIFFERENCE,
282+ metadata=make_metadata(sourcepackagename))
283+ IMasterStore(DistributionJob).add(job)
284+ return DistroSeriesDifferenceJob(job)
285+
286+
287+def find_waiting_jobs(distroseries, sourcepackagename):
288+ """Look for pending `DistroSeriesDifference` jobs on a package."""
289+ # Look for identical pending jobs. This compares directly on
290+ # the metadata string. It's fragile, but this is only an
291+ # optimization. It's not actually disastrous to create
292+ # redundant jobs occasionally.
293+ json_metadata = DistributionJob.serializeMetadata(
294+ make_metadata(sourcepackagename))
295+
296+ # Use master store because we don't like outdated information
297+ # here.
298+ store = IMasterStore(DistributionJob)
299+
300+ return store.find(
301+ DistributionJob,
302+ DistributionJob.job_type ==
303+ DistributionJobType.DISTROSERIESDIFFERENCE,
304+ DistributionJob.distroseries == distroseries,
305+ DistributionJob._json_data == json_metadata,
306+ DistributionJob.job_id.is_in(Job.ready_jobs))
307+
308+
309+def may_require_job(distroseries, sourcepackagename):
310+ """Might publishing this package require a new job?
311+
312+ Use this to determine whether to create a new
313+ `DistroSeriesDifferenceJob`. The answer may possibly be
314+ conservatively wrong: the check is really only to save the job
315+ runner some unnecessary work, but we don't expect a bit of
316+ unnecessary work to be a big problem.
317+ """
318+ if distroseries is None:
319+ return False
320+ parent_series = distroseries.parent_series
321+ if parent_series is None:
322+ return False
323+ if parent_series.distribution == distroseries.distribution:
324+ # Differences within a distribution are not tracked.
325+ return False
326+ return find_waiting_jobs(distroseries, sourcepackagename).is_empty()
327+
328+
329+class DistroSeriesDifferenceJob(DistributionJobDerived):
330+ """A `Job` type for creating/updating `DistroSeriesDifference`s."""
331+
332+ implements(IDistributionJob)
333+ classProvides(IDistroSeriesDifferenceJobSource)
334+
335+ class_job_type = DistributionJobType.DISTROSERIESDIFFERENCE
336+
337+ @classmethod
338+ def createForPackagePublication(cls, distroseries, sourcepackagename):
339+ """See `IDistroSeriesDifferenceJobSource`."""
340+ children = distroseries.getDerivedSeries()
341+ parent = distroseries.parent_series
342+ for relative in list(children) + [parent]:
343+ if may_require_job(relative, sourcepackagename):
344+ create_job(relative, sourcepackagename)
345+
346+ def run(self):
347+ """See `IRunnableJob`."""
348+# TODO: Implement the business end.
349
350=== modified file 'lib/lp/soyuz/model/publishing.py'
351--- lib/lp/soyuz/model/publishing.py 2011-03-08 16:42:43 +0000
352+++ lib/lp/soyuz/model/publishing.py 2011-03-11 07:06:49 +0000
353@@ -85,6 +85,9 @@
354 BuildSetStatus,
355 IBinaryPackageBuildSet,
356 )
357+from lp.soyuz.interfaces.distroseriesdifferencejob import (
358+ IDistroSeriesDifferenceJobSource,
359+ )
360 from lp.soyuz.interfaces.publishing import (
361 active_publishing_status,
362 IBinaryPackageFilePublishing,
363@@ -1422,6 +1425,10 @@
364 datecreated=UTC_NOW,
365 ancestor=ancestor)
366 DistributionSourcePackage.ensure(pub)
367+
368+ dsd_job_source = getUtility(IDistroSeriesDifferenceJobSource)
369+ dsd_job_source.createForPackagePublication(
370+ distroseries, sourcepackagerelease.sourcepackagename)
371 return pub
372
373 def getBuildsForSourceIds(
374
375=== added file 'lib/lp/soyuz/tests/test_distroseriesdifferencejob.py'
376--- lib/lp/soyuz/tests/test_distroseriesdifferencejob.py 1970-01-01 00:00:00 +0000
377+++ lib/lp/soyuz/tests/test_distroseriesdifferencejob.py 2011-03-11 07:06:49 +0000
378@@ -0,0 +1,146 @@
379+# Copyright 2011 Canonical Ltd. This software is licensed under the
380+# GNU Affero General Public License version 3 (see the file LICENSE).
381+
382+"""Test `DistroSeriesDifferenceJob` and utility."""
383+
384+__metaclass__ = type
385+
386+from zope.component import getUtility
387+from zope.interface.verify import verifyObject
388+
389+from canonical.testing.layers import ZopelessDatabaseLayer
390+from lp.services.job.interfaces.job import JobStatus
391+from lp.soyuz.interfaces.distroseriesdifferencejob import (
392+ IDistroSeriesDifferenceJobSource,
393+ )
394+from lp.soyuz.model.distroseriesdifferencejob import (
395+ create_job,
396+ find_waiting_jobs,
397+ make_metadata,
398+ may_require_job,
399+ )
400+from lp.testing import TestCaseWithFactory
401+
402+
403+class TestDistroSeriesDifferenceJobSource(TestCaseWithFactory):
404+ """Tests for `IDistroSeriesDifferenceJobSource`."""
405+
406+ layer = ZopelessDatabaseLayer
407+
408+ def getJobSource(self):
409+ return getUtility(IDistroSeriesDifferenceJobSource)
410+
411+ def makeDerivedDistroSeries(self):
412+ return self.factory.makeDistroSeries(
413+ parent_series=self.factory.makeDistroSeries())
414+
415+ def test_baseline(self):
416+ verifyObject(IDistroSeriesDifferenceJobSource, self.getJobSource())
417+
418+ def test_make_metadata_is_consistent(self):
419+ package = self.factory.makeSourcePackageName()
420+ self.assertEqual(make_metadata(package), make_metadata(package))
421+
422+ def test_make_metadata_distinguishes_packages(self):
423+ one_package = self.factory.makeSourcePackageName()
424+ another_package = self.factory.makeSourcePackageName()
425+ self.assertNotEqual(
426+ make_metadata(one_package), make_metadata(another_package))
427+
428+ def test_may_require_job_accepts_none_distroseries(self):
429+ package = self.factory.makeSourcePackageName()
430+ self.assertFalse(may_require_job(None, package))
431+
432+ def test_may_require_job_allows_new_jobs(self):
433+ distroseries = self.makeDerivedDistroSeries()
434+ package = self.factory.makeSourcePackageName()
435+ self.assertTrue(may_require_job(distroseries, package))
436+
437+ def test_may_require_job_forbids_redundant_jobs(self):
438+ distroseries = self.makeDerivedDistroSeries()
439+ package = self.factory.makeSourcePackageName()
440+ create_job(distroseries, package)
441+ self.assertFalse(may_require_job(distroseries, package))
442+
443+ def test_may_require_job_forbids_jobs_on_nonderived_series(self):
444+ sourcepackage = self.factory.makeSourcePackage()
445+ self.assertFalse(may_require_job(
446+ sourcepackage.distroseries, sourcepackage.sourcepackagename))
447+
448+ def test_may_require_job_forbids_jobs_for_intra_distro_derivation(self):
449+ package = self.factory.makeSourcePackageName()
450+ parent = self.factory.makeDistroSeries()
451+ child = self.factory.makeDistroSeries(
452+ distribution=parent.distribution, parent_series=parent)
453+ self.assertFalse(may_require_job(child, package))
454+
455+ def test_may_require_job_only_considers_waiting_jobs_for_redundancy(self):
456+ distroseries = self.makeDerivedDistroSeries()
457+ package = self.factory.makeSourcePackageName()
458+ existing_job = create_job(distroseries, package)
459+ existing_job.job.start()
460+ self.assertTrue(may_require_job(distroseries, package))
461+
462+ def test_create_job_creates_waiting_job(self):
463+ distroseries = self.makeDerivedDistroSeries()
464+ package = self.factory.makeSourcePackageName()
465+ dsdjob = create_job(distroseries, package)
466+ self.assertEqual(JobStatus.WAITING, dsdjob.job.status)
467+
468+ def find_waiting_jobs_finds_waiting_jobs(self):
469+ sourcepackage = self.factory.makeSourcePackage()
470+ distroseries, sourcepackagename = (
471+ sourcepackage.distroseries, sourcepackage.distroseries)
472+ job = create_job(distroseries, sourcepackagename)
473+ self.assertContentEqual(
474+ [job], find_waiting_jobs(distroseries, sourcepackagename))
475+
476+ def find_waiting_jobs_ignores_other_series(self):
477+ sourcepackage = self.factory.makeSourcePackage()
478+ distroseries, sourcepackagename = (
479+ sourcepackage.distroseries, sourcepackage.distroseries)
480+ job = create_job(distroseries, sourcepackagename)
481+ other_series = self.factory.makeDistroSeries()
482+ self.assertContentEqual(
483+ [], find_waiting_jobs(other_series, sourcepackagename))
484+
485+ def find_waiting_jobs_ignores_other_packages(self):
486+ sourcepackage = self.factory.makeSourcePackage()
487+ distroseries, sourcepackagename = (
488+ sourcepackage.distroseries, sourcepackage.distroseries)
489+ job = create_job(distroseries, sourcepackagename)
490+ other_spn = self.factory.makeSourcePackageName()
491+ self.assertContentEqual(
492+ [], find_waiting_jobs(distroseries, other_spn))
493+
494+ def find_waiting_jobs_considers_only_waiting_jobs(self):
495+ sourcepackage = self.factory.makeSourcePackage()
496+ distroseries, sourcepackagename = (
497+ sourcepackage.distroseries, sourcepackage.distroseries)
498+ job = create_job(distroseries, sourcepackagename)
499+ job.start()
500+ self.assertContentEqual(
501+ [], find_waiting_jobs(distroseries, sourcepackagename))
502+ job.complete()
503+ self.assertContentEqual(
504+ [], find_waiting_jobs(distroseries, sourcepackagename))
505+
506+ def test_createForPackagedPublication_creates_job_for_parent_series(self):
507+ derived_series = self.factory.makeDistroSeries(
508+ parent_series=self.makeDerivedDistroSeries())
509+ package = self.factory.makeSourcePackageName()
510+ self.getJobSource().createForPackagePublication(
511+ derived_series, package)
512+ jobs = list(find_waiting_jobs(derived_series.parent_series, package))
513+ self.assertEqual(1, len(jobs))
514+ self.assertEqual(package.id, jobs[0].metadata['sourcepackagename'])
515+
516+ def test_createForPackagePublication_creates_job_for_derived_series(self):
517+ derived_series = self.makeDerivedDistroSeries()
518+ parent_series = derived_series.parent_series
519+ package = self.factory.makeSourcePackageName()
520+ self.getJobSource().createForPackagePublication(
521+ parent_series, package)
522+ jobs = list(find_waiting_jobs(derived_series, package))
523+ self.assertEqual(1, len(jobs))
524+ self.assertEqual(package.id, jobs[0].metadata['sourcepackagename'])