Merge lp:~jtv/launchpad/db-bug-786970 into lp:launchpad/db-devel

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 10617
Proposed branch: lp:~jtv/launchpad/db-bug-786970
Merge into: lp:launchpad/db-devel
Diff against target: 559 lines (+211/-115)
10 files modified
database/schema/security.cfg (+6/-0)
lib/lp/registry/browser/distroseries.py (+8/-11)
lib/lp/registry/browser/tests/test_distroseries.py (+7/-3)
lib/lp/registry/interfaces/distroseriesdifference.py (+0/-12)
lib/lp/registry/model/distroseriesdifference.py (+0/-11)
lib/lp/registry/tests/test_distroseriesdifference.py (+0/-18)
lib/lp/soyuz/browser/archive.py (+3/-16)
lib/lp/soyuz/browser/tests/test_package_copying_mixin.py (+0/-25)
lib/lp/soyuz/model/packagecopyjob.py (+62/-0)
lib/lp/soyuz/tests/test_packagecopyjob.py (+125/-19)
To merge this branch: bzr merge lp:~jtv/launchpad/db-bug-786970
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+62292@code.launchpad.net

Commit message

[r=allenap][bug=786970,787462] Register CannotCopy failures as DSDComments.

Description of the change

= Summary =

When a PlainPackageCopyJob fails with a CannotCopy error, that's not really meant to be an oops. It's meant to be an error that the user should address. But how do we tell the user? This job is something you "see going on" in the UI, so we want the errors in the UI as well — email is not an answer.

== Proposed fix ==

Julian and I (with feedback from Colin) decided on the following error-reporting mechanism: any one of the PlainPackageCopyJobs we're dealing with resolves a DistroSeriesDifference, which is what's visible in the UI. And that in turn can have a conversation stream of DistroSeriesDifferenceComments attached. So the job can throw CannotCopy failure messages into that stream, which also solves the problem of logging historical builds.

Next we'll make the page poll, so that these messages can show up on the fly.

== Pre-implementation notes ==

Multiple package copies for multiple DistroSeriesDifferences can go into a single PlainPackageCopyJob. We initially thought we could capture enough information from failures that a CannotCopy can be neatly pegged to a DistroSeriesDifference on the page.

But alas. There are complications: it takes some relatively complicated tracking, and because the checks are to some extent phased, you might see your package failing because some other package broke the whole job. In the end we decided on a simple fix: only handle one package per job. This affects two callsites.

Another question is: what Launchpad user will the error messages be attributed to? An attempt to start a discussion about having a user identity to represent Launchpad in the database for this sort of thing failed miserably. We thought it might be easy impersonate the requesting user (which will be a little disconcerting because automatically generated messages meant primarily for the user will look as if they'd been entered by that same user). But the only current place for that information is Job.requester, which… isn't accessible in Python. In the end Julian gave me the green light to use the Janitor, which fulfills many of the roles that I had wanted a "Launchpad persona" user for.

== Implementation details ==

For once I remembered to test the new code for database privileges. Which revealed a few that were needed, hopefully saving us some agony later on.

Sadly gone are very similar bits of code that Gavin and I developed in parallel for collating packages by archive, so as to bundle them into the minimum number of PlainPackageCopyJobs.

== Tests ==

{{{
./bin/test -vvc lp.soyuz.tests.test_packagecopyjob
}}}

== Demo and Q/A ==

Request asynchronous package copies on a derived distroseries' +localpackagediffs page. (Do this by requesting more copies than the max_synchronous_syncs feature flag setting at once). If any of them fail (and I admit I have yet to figure out how to do that) its error will show up as a comment.

= Launchpad lint =

No lint, but it's complicated. I touched a file that had pre-existing lint, which I did not fix, and then I undid my changes to that file.

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

This looks really good.

However, I have a major beef with it: PlainPackageCopyJob is no longer
a plain copy job; it's now heavily intertwined with derivative
distributions. I think PlainPackageCopyJob should be restored, and a
new subclass created, something like DerivativePackageCopyJob, that
knows about DSDs and so forth. It's unfortunate that that involves a
lot of boilerplate code.

Other than that, I think it's a shame that packages can only be copied
one at a time, but I can see this is a far from trivial problem to
solve. It does mean that there's no longer an all-or-nothing guarantee
when copying multiple packages. A *big* downside is that users must
check all DSD comments for errors when they request multiple copies. I
assume both of those are acceptable compromises for now.

[1]

+ try:
+ self.attemptCopy()
+ except CannotCopy, e:
+ self.abort()
+ self.reportFailure(e)
+ self.commit()

Fwiw, the job runner commits after run(), so only the abort() is
needed.

[2]

+ def findMatchingDSDs(self):
[...]
+ source_distro = self.source_archive.distribution
+ return [
+ dsd
+ for dsd in candidates
+ if dsd.parent_series.distribution == source_distro]

Can you do with with IDs instead?

        source_distro_id = self.source_archive.distributionID
        return [
            dsd
            for dsd in candidates
                if dsd.parent_series.distributionID == source_distro_id]

[3]

+ def test_findMatchingDSDs_matches_all_DSDs_for_job(self):
+ # findMatchingDSDs finds matching DSDs for any of the packages
+ # in the job.
+ dsd = self.factory.makeDistroSeriesDifference()
+ job = removeSecurityProxy(self.makeJob(dsd))

This should be naked_job.

Also in test_findMatchingDSDs_ignores_other_source_series.

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

Thanks Gavin.

> However, I have a major beef with it: PlainPackageCopyJob is no longer
> a plain copy job; it's now heavily intertwined with derivative
> distributions. I think PlainPackageCopyJob should be restored, and a
> new subclass created, something like DerivativePackageCopyJob, that
> knows about DSDs and so forth. It's unfortunate that that involves a
> lot of boilerplate code.

That seems like a job for a separate branch though; it will affect a lot more than what's in this branch.

Do I understand that correctly? If so, it seems easier to land this branch first and then make a separate card of splitting up the various usages of PPCJ. Would be nice to get the present branch off my hands!

> Other than that, I think it's a shame that packages can only be copied
> one at a time, but I can see this is a far from trivial problem to
> solve. It does mean that there's no longer an all-or-nothing guarantee
> when copying multiple packages. A *big* downside is that users must
> check all DSD comments for errors when they request multiple copies. I
> assume both of those are acceptable compromises for now.

Yes— we considered that, but it turns out that multi-parent breaks that anyway. You could be selecting DSDs from different parent archives, which have to go into separate jobs regardless!

So in the final analysis we can blame this on the post-last-minute change in requirements.

> + try:
> + self.attemptCopy()
> + except CannotCopy, e:
> + self.abort()
> + self.reportFailure(e)
> + self.commit()
>
> Fwiw, the job runner commits after run(), so only the abort() is
> needed.

Nice one! That commit was there from a time when the exception was still propagated, which I preserved in this branch initially but was then rightly asked to change.

> + def findMatchingDSDs(self):
> [...]
> + source_distro = self.source_archive.distribution
> + return [
> + dsd
> + for dsd in candidates
> + if dsd.parent_series.distribution == source_distro]
>
> Can you do with with IDs instead?

Sure. Silly thing that the ORM needs to fetch the distribution there, really.

> + def test_findMatchingDSDs_matches_all_DSDs_for_job(self):
> + # findMatchingDSDs finds matching DSDs for any of the packages
> + # in the job.
> + dsd = self.factory.makeDistroSeriesDifference()
> + job = removeSecurityProxy(self.makeJob(dsd))
>
> This should be naked_job.
>
> Also in test_findMatchingDSDs_ignores_other_source_series.

Fixed.

Jeroen

Revision history for this message
Gavin Panella (allenap) wrote :

> Thanks Gavin.
>
> > However, I have a major beef with it: PlainPackageCopyJob is no longer
> > a plain copy job; it's now heavily intertwined with derivative
> > distributions. I think PlainPackageCopyJob should be restored, and a
> > new subclass created, something like DerivativePackageCopyJob, that
> > knows about DSDs and so forth. It's unfortunate that that involves a
> > lot of boilerplate code.
>
> That seems like a job for a separate branch though; it will affect a lot more
> than what's in this branch.
>
> Do I understand that correctly? If so, it seems easier to land this branch
> first and then make a separate card of splitting up the various usages of
> PPCJ. Would be nice to get the present branch off my hands!

As agreed otp just now (between bigjools, jtv and allenap), it's cool
to leave this branch as it is and things can be split out again if and
when a solid requirement is there.

>
>
> > Other than that, I think it's a shame that packages can only be copied
> > one at a time, but I can see this is a far from trivial problem to
> > solve. It does mean that there's no longer an all-or-nothing guarantee
> > when copying multiple packages. A *big* downside is that users must
> > check all DSD comments for errors when they request multiple copies. I
> > assume both of those are acceptable compromises for now.
>
> Yes— we considered that, but it turns out that multi-parent breaks that
> anyway. You could be selecting DSDs from different parent archives, which
> have to go into separate jobs regardless!

Of course, that's very true, I didn't think of that.

>
> So in the final analysis we can blame this on the post-last-minute change in
> requirements.

Yes! Grumble, grumble.

review: Approve

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-05-25 17:29:39 +0000
3+++ database/schema/security.cfg 2011-05-28 06:24:35 +0000
4@@ -984,6 +984,7 @@
5
6 [sync_packages]
7 groups=script
8+public.account = SELECT
9 public.archive = SELECT
10 public.archivepermission = SELECT, INSERT
11 public.binarypackagebuild = SELECT, INSERT
12@@ -1001,12 +1002,17 @@
13 public.distributionsourcepackage = SELECT, INSERT, UPDATE
14 public.distroarchseries = SELECT, INSERT
15 public.distroseries = SELECT, UPDATE
16+public.distroseriesdifference = SELECT
17+public.distroseriesdifferencemessage = SELECT, INSERT, UPDATE
18 public.distroseriesparent = SELECT
19+public.emailaddress = SELECT
20 public.flatpackagesetinclusion = SELECT, INSERT
21 public.gpgkey = SELECT
22 public.job = SELECT, INSERT, UPDATE, DELETE
23 public.libraryfilealias = SELECT, INSERT, UPDATE, DELETE
24 public.libraryfilecontent = SELECT, INSERT
25+public.message = SELECT, INSERT, UPDATE
26+public.messagechunk = SELECT, INSERT, UPDATE
27 public.packagebuild = SELECT, INSERT
28 public.packagecopyjob = SELECT
29 public.packageset = SELECT, INSERT
30
31=== modified file 'lib/lp/registry/browser/distroseries.py'
32--- lib/lp/registry/browser/distroseries.py 2011-05-27 08:02:37 +0000
33+++ lib/lp/registry/browser/distroseries.py 2011-05-28 06:24:35 +0000
34@@ -1078,17 +1078,14 @@
35 """Request sync of packages that can be easily upgraded."""
36 target_distroseries = self.context
37 target_archive = target_distroseries.main_archive
38- differences_by_archive = (
39- getUtility(IDistroSeriesDifferenceSource)
40- .collateDifferencesByParentArchive(self.getUpgrades()))
41- for source_archive, differences in differences_by_archive.iteritems():
42- source_package_info = [
43- (difference.source_package_name.name,
44- difference.parent_source_version)
45- for difference in differences]
46- getUtility(IPlainPackageCopyJobSource).create(
47- source_package_info, source_archive, target_archive,
48- target_distroseries, PackagePublishingPocket.UPDATES)
49+ job_source = getUtility(IPlainPackageCopyJobSource)
50+
51+ for dsd in self.getUpgrades():
52+ job_source.create(
53+ [specify_dsd_package(dsd)], dsd.parent_series.main_archive,
54+ target_archive, target_distroseries,
55+ PackagePublishingPocket.UPDATES)
56+
57 self.request.response.addInfoNotification(
58 (u"Upgrades of {context.displayname} packages have been "
59 u"requested. Please give Launchpad some time to complete "
60
61=== modified file 'lib/lp/registry/browser/tests/test_distroseries.py'
62--- lib/lp/registry/browser/tests/test_distroseries.py 2011-05-27 07:50:38 +0000
63+++ lib/lp/registry/browser/tests/test_distroseries.py 2011-05-28 06:24:35 +0000
64@@ -1090,13 +1090,17 @@
65 with StormStatementRecorder() as recorder1:
66 self.makeView(derived_series).requestUpgrades()
67 self.assertThat(recorder1, HasQueryCount(LessThan(10)))
68- # The query count does not increase with more differences.
69- for index in xrange(3):
70+ # Creating Jobs and DistributionJobs takes 2 extra queries per
71+ # requested sync.
72+ requested_syncs = 3
73+ for index in xrange(requested_syncs):
74 self.makePackageUpgrade(derived_series=derived_series)
75 flush_database_caches()
76 with StormStatementRecorder() as recorder2:
77 self.makeView(derived_series).requestUpgrades()
78- self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
79+ self.assertThat(
80+ recorder2,
81+ HasQueryCount(Equals(recorder1.count + 2 * requested_syncs)))
82
83
84 class TestDistroSeriesLocalDifferencesFunctional(TestCaseWithFactory,
85
86=== modified file 'lib/lp/registry/interfaces/distroseriesdifference.py'
87--- lib/lp/registry/interfaces/distroseriesdifference.py 2011-05-24 15:36:35 +0000
88+++ lib/lp/registry/interfaces/distroseriesdifference.py 2011-05-28 06:24:35 +0000
89@@ -338,15 +338,3 @@
90
91 Blacklisted items are excluded.
92 """
93-
94- def collateDifferencesByParentArchive(differences):
95- """Collate the given differences by parent archive.
96-
97- The given `IDistroSeriesDifference`s are returned in a `dict`, with
98- the parent `Archive` as keys.
99-
100- :param differences: An iterable sequence of `IDistroSeriesDifference`.
101-
102- :return: A `dict` of iterable sequences of `IDistroSeriesDifference`
103- keyed by their parent `IArchive`.
104- """
105
106=== modified file 'lib/lp/registry/model/distroseriesdifference.py'
107--- lib/lp/registry/model/distroseriesdifference.py 2011-05-24 11:32:34 +0000
108+++ lib/lp/registry/model/distroseriesdifference.py 2011-05-28 06:24:35 +0000
109@@ -494,17 +494,6 @@
110 DistroSeriesDifference.source_package_name_id)
111 return DecoratedResultSet(differences, itemgetter(0))
112
113- @staticmethod
114- def collateDifferencesByParentArchive(differences):
115- by_archive = dict()
116- for difference in differences:
117- archive = difference.parent_series.main_archive
118- if archive in by_archive:
119- by_archive[archive].append(difference)
120- else:
121- by_archive[archive] = [difference]
122- return by_archive
123-
124 @cachedproperty
125 def source_pub(self):
126 """See `IDistroSeriesDifference`."""
127
128=== modified file 'lib/lp/registry/tests/test_distroseriesdifference.py'
129--- lib/lp/registry/tests/test_distroseriesdifference.py 2011-05-24 15:36:35 +0000
130+++ lib/lp/registry/tests/test_distroseriesdifference.py 2011-05-28 06:24:35 +0000
131@@ -1129,24 +1129,6 @@
132 self.assertContentEqual(
133 [], dsd_source.getSimpleUpgrades(dsd.derived_series))
134
135- def test_collateDifferencesByParentArchive(self):
136- dsp1 = self.factory.makeDistroSeriesParent()
137- dsp2 = self.factory.makeDistroSeriesParent()
138- differences = [
139- self.factory.makeDistroSeriesDifference(dsp1.derived_series),
140- self.factory.makeDistroSeriesDifference(dsp2.derived_series),
141- self.factory.makeDistroSeriesDifference(dsp1.derived_series),
142- self.factory.makeDistroSeriesDifference(dsp2.derived_series),
143- ]
144- dsd_source = getUtility(IDistroSeriesDifferenceSource)
145- observed = (
146- dsd_source.collateDifferencesByParentArchive(differences))
147- expected = {
148- dsp1.parent_series.main_archive: differences[0::2],
149- dsp2.parent_series.main_archive: differences[1::2],
150- }
151- self.assertEqual(observed, expected)
152-
153
154 class TestMostRecentComments(TestCaseWithFactory):
155
156
157=== modified file 'lib/lp/soyuz/browser/archive.py'
158--- lib/lp/soyuz/browser/archive.py 2011-05-26 15:06:44 +0000
159+++ lib/lp/soyuz/browser/archive.py 2011-05-28 06:24:35 +0000
160@@ -1285,19 +1285,6 @@
161 dest_display_name)
162
163
164-def partition_pubs_by_archive(source_pubs):
165- """Group `source_pubs` by archive.
166-
167- :param source_pubs: A sequence of `SourcePackagePublishingHistory`.
168- :return: A dict mapping `Archive`s to the list of entries from
169- `source_pubs` that are in that archive.
170- """
171- by_source_archive = {}
172- for spph in source_pubs:
173- by_source_archive.setdefault(spph.archive, []).append(spph)
174- return by_source_archive
175-
176-
177 def name_pubs_with_versions(source_pubs):
178 """Annotate each entry from `source_pubs` with its version.
179
180@@ -1328,11 +1315,11 @@
181 person, dest_archive, dest_series, dest_pocket, spns)
182
183 job_source = getUtility(IPlainPackageCopyJobSource)
184- archive_pubs = partition_pubs_by_archive(source_pubs)
185- for source_archive, spphs in archive_pubs.iteritems():
186+ for spph in source_pubs:
187 job_source.create(
188- name_pubs_with_versions(spphs), source_archive, dest_archive,
189+ name_pubs_with_versions([spph]), spph.archive, dest_archive,
190 dest_series, dest_pocket, include_binaries=include_binaries)
191+
192 return structured("""
193 <p>Requested sync of %s packages.</p>
194 <p>Please allow some time for these to be processed.</p>
195
196=== modified file 'lib/lp/soyuz/browser/tests/test_package_copying_mixin.py'
197--- lib/lp/soyuz/browser/tests/test_package_copying_mixin.py 2011-05-20 20:40:19 +0000
198+++ lib/lp/soyuz/browser/tests/test_package_copying_mixin.py 2011-05-28 06:24:35 +0000
199@@ -19,7 +19,6 @@
200 FEATURE_FLAG_MAX_SYNCHRONOUS_SYNCS,
201 name_pubs_with_versions,
202 PackageCopyingMixin,
203- partition_pubs_by_archive,
204 render_cannotcopy_as_html,
205 )
206 from lp.soyuz.interfaces.archive import CannotCopy
207@@ -104,30 +103,6 @@
208 packages = [self.getUniqueString() for counter in range(300)]
209 self.assertFalse(PackageCopyingMixin().canCopySynchronously(packages))
210
211- def test_partition_pubs_by_archive_maps_archives_to_pubs(self):
212- # partition_pubs_by_archive returns a dict mapping each archive
213- # to a list of SPPHs on that archive.
214- spph = FakeSPPH()
215- self.assertEqual(
216- {spph.archive: [spph]}, partition_pubs_by_archive([spph]))
217-
218- def test_partition_pubs_by_archive_splits_by_archive(self):
219- # partition_pubs_by_archive keeps SPPHs for different archives
220- # separate.
221- spphs = [FakeSPPH() for counter in xrange(2)]
222- mapping = partition_pubs_by_archive(spphs)
223- self.assertEqual(
224- dict((spph.archive, [spph]) for spph in spphs), mapping)
225-
226- def test_partition_pubs_by_archive_clusters_by_archive(self):
227- # partition_pubs_by_archive bundles SPPHs for the same archive
228- # into a single dict entry.
229- archive = FakeArchive()
230- spphs = [FakeSPPH(archive=archive) for counter in xrange(2)]
231- mapping = partition_pubs_by_archive(spphs)
232- self.assertEqual([archive], mapping.keys())
233- self.assertContentEqual(spphs, mapping[archive])
234-
235 def test_name_pubs_with_versions_lists_packages_and_versions(self):
236 # name_pubs_with_versions returns a list of tuples of source
237 # package name and source package version, one per SPPH.
238
239=== modified file 'lib/lp/soyuz/model/packagecopyjob.py'
240--- lib/lp/soyuz/model/packagecopyjob.py 2011-05-19 17:20:02 +0000
241+++ lib/lp/soyuz/model/packagecopyjob.py 2011-05-28 06:24:35 +0000
242@@ -17,6 +17,8 @@
243 Reference,
244 Unicode,
245 )
246+import transaction
247+from zope.component import getUtility
248 from zope.interface import (
249 classProvides,
250 implements,
251@@ -26,13 +28,20 @@
252 from canonical.launchpad.components.decoratedresultset import (
253 DecoratedResultSet,
254 )
255+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
256 from canonical.launchpad.interfaces.lpstorm import (
257 IMasterStore,
258 IStore,
259 )
260 from lp.app.errors import NotFoundError
261+from lp.registry.interfaces.distroseriesdifference import (
262+ IDistroSeriesDifferenceSource,
263+ )
264 from lp.registry.interfaces.pocket import PackagePublishingPocket
265 from lp.registry.model.distroseries import DistroSeries
266+from lp.registry.interfaces.distroseriesdifferencecomment import (
267+ IDistroSeriesDifferenceCommentSource,
268+ )
269 from lp.services.database.stormbase import StormBase
270 from lp.services.job.interfaces.job import JobStatus
271 from lp.services.job.model.job import Job
272@@ -153,6 +162,11 @@
273
274 class PlainPackageCopyJob(PackageCopyJobDerived):
275 """Job that copies packages between archives."""
276+ # This job type serves in different places: it supports copying
277+ # packages between archives, but also the syncing of packages from
278+ # parents into a derived distroseries. We may split these into
279+ # separate types at some point, but for now we (allenap, bigjools,
280+ # jtv) chose to keep it as one.
281
282 implements(IPlainPackageCopyJob)
283
284@@ -232,6 +246,18 @@
285
286 def run(self):
287 """See `IRunnableJob`."""
288+ try:
289+ self.attemptCopy()
290+ except CannotCopy, e:
291+ self.abort()
292+ self.reportFailure(e)
293+
294+ def attemptCopy(self):
295+ """Attempt to perform the copy.
296+
297+ :raise CannotCopy: If the copy fails for a reason that the user
298+ can deal with.
299+ """
300 if self.target_archive.is_ppa:
301 if self.target_pocket != PackagePublishingPocket.RELEASE:
302 raise CannotCopy(
303@@ -250,6 +276,42 @@
304 series=self.target_distroseries, pocket=self.target_pocket,
305 include_binaries=self.include_binaries, check_permissions=False)
306
307+ def abort(self):
308+ """Abort work."""
309+ transaction.abort()
310+
311+ def findMatchingDSDs(self):
312+ """Find any `DistroSeriesDifference`s that this job might resolve."""
313+ dsd_source = getUtility(IDistroSeriesDifferenceSource)
314+ target_series = self.target_distroseries
315+ candidates = dsd_source.getForDistroSeries(
316+ distro_series=target_series)
317+ # The job doesn't know what distroseries a given package is
318+ # coming from, and the version number in the DSD may have
319+ # changed. We can however filter out DSDs that are from
320+ # different distributions, based on the job's target archive.
321+ source_distro_id = self.source_archive.distributionID
322+ return [
323+ dsd
324+ for dsd in candidates
325+ if dsd.parent_series.distributionID == source_distro_id]
326+
327+ def reportFailure(self, cannotcopy_exception):
328+ """Attempt to report failure to the user."""
329+ message = unicode(cannotcopy_exception)
330+ dsds = self.findMatchingDSDs()
331+ comment_source = getUtility(IDistroSeriesDifferenceCommentSource)
332+
333+ # Register the error comment in the name of the Janitor. Not a
334+ # great choice, but we have no user identity to represent
335+ # Launchpad; it's far too costly to create one; and
336+ # impersonating the requester can be misleading and would also
337+ # involve extra bookkeeping.
338+ reporting_persona = getUtility(ILaunchpadCelebrities).janitor
339+
340+ for dsd in dsds:
341+ comment_source.new(dsd, reporting_persona, message)
342+
343 def __repr__(self):
344 """Returns an informative representation of the job."""
345 parts = ["%s to copy" % self.__class__.__name__]
346
347=== modified file 'lib/lp/soyuz/tests/test_packagecopyjob.py'
348--- lib/lp/soyuz/tests/test_packagecopyjob.py 2011-05-24 14:29:51 +0000
349+++ lib/lp/soyuz/tests/test_packagecopyjob.py 2011-05-28 06:24:35 +0000
350@@ -8,7 +8,12 @@
351 from zope.component import getUtility
352 from zope.security.proxy import removeSecurityProxy
353
354+from canonical.config import config
355+from canonical.launchpad.interfaces.lpstorm import IStore
356 from canonical.testing import LaunchpadZopelessLayer
357+from lp.registry.model.distroseriesdifferencecomment import (
358+ DistroSeriesDifferenceComment,
359+ )
360 from lp.registry.interfaces.pocket import PackagePublishingPocket
361 from lp.services.features.testing import FeatureFixture
362 from lp.services.job.interfaces.job import JobStatus
363@@ -34,15 +39,23 @@
364 run_script,
365 TestCaseWithFactory,
366 )
367-
368-
369-class PlainPackageCopyJobTests(TestCaseWithFactory):
370- """Test case for PlainPackageCopyJob."""
371-
372- layer = LaunchpadZopelessLayer
373-
374- def makeJob(self, dsd):
375+from lp.testing.fakemethod import FakeMethod
376+
377+
378+def get_dsd_comments(dsd):
379+ """Retrieve `DistroSeriesDifferenceComment`s for `dsd`."""
380+ return IStore(dsd).find(
381+ DistroSeriesDifferenceComment,
382+ DistroSeriesDifferenceComment.distro_series_difference == dsd)
383+
384+
385+class LocalTestHelper:
386+ """Put test helpers that want to be in the test classes here."""
387+
388+ def makeJob(self, dsd=None):
389 """Create a `PlainPackageCopyJob` that would resolve `dsd`."""
390+ if dsd is None:
391+ dsd = self.factory.makeDistroSeriesDifference()
392 source_packages = [specify_dsd_package(dsd)]
393 source_archive = dsd.parent_series.main_archive
394 target_archive = dsd.derived_series.main_archive
395@@ -58,6 +71,12 @@
396 self.layer.switchDbUser('sync_packages')
397 job.run()
398
399+
400+class PlainPackageCopyJobTests(TestCaseWithFactory, LocalTestHelper):
401+ """Test case for PlainPackageCopyJob."""
402+
403+ layer = LaunchpadZopelessLayer
404+
405 def test_create(self):
406 # A PackageCopyJob can be created and stores its arguments.
407 distroseries = self.factory.makeDistroSeries()
408@@ -105,23 +124,59 @@
409
410 def test_getActiveJobs_only_returns_waiting_jobs(self):
411 # getActiveJobs ignores jobs that aren't in the WAITING state.
412- job = self.makeJob(self.factory.makeDistroSeriesDifference())
413+ job = self.makeJob()
414 removeSecurityProxy(job).job._status = JobStatus.RUNNING
415 source = getUtility(IPlainPackageCopyJobSource)
416 self.assertContentEqual([], source.getActiveJobs(job.target_archive))
417
418- def test_run_unknown_package(self):
419- # A job properly records failure.
420+ def test_run_raises_errors(self):
421+ # A job reports unexpected errors as exceptions.
422+ class Boom(Exception):
423+ pass
424+
425+ job = self.makeJob()
426+ removeSecurityProxy(job).attemptCopy = FakeMethod(failure=Boom())
427+
428+ self.assertRaises(Boom, job.run)
429+
430+ def test_run_posts_copy_failure_as_comment(self):
431+ # If the job fails with a CannotCopy exception, it swallows the
432+ # exception and posts a DistroSeriesDifferenceComment with the
433+ # failure message.
434+ dsd = self.factory.makeDistroSeriesDifference()
435+ self.factory.makeArchive(distribution=dsd.derived_series.distribution)
436+ job = self.makeJob(dsd)
437+ removeSecurityProxy(job).attemptCopy = FakeMethod(
438+ failure=CannotCopy("Server meltdown"))
439+
440+ # The job's error handling will abort, so commit the objects we
441+ # created as would have happened in real life.
442+ transaction.commit()
443+
444+ job.run()
445+
446+ self.assertEqual(
447+ ["Server meltdown"],
448+ [comment.body_text for comment in get_dsd_comments(dsd)])
449+
450+ def test_run_cannot_copy_unknown_package(self):
451+ # Attempting to copy an unknown package is reported as a
452+ # failure.
453 distroseries = self.factory.makeDistroSeries()
454 archive1 = self.factory.makeArchive(distroseries.distribution)
455 archive2 = self.factory.makeArchive(distroseries.distribution)
456- source = getUtility(IPlainPackageCopyJobSource)
457- job = source.create(
458+ job_source = getUtility(IPlainPackageCopyJobSource)
459+ job = job_source.create(
460 source_packages=[("foo", "1.0-1")], source_archive=archive1,
461 target_archive=archive2, target_distroseries=distroseries,
462 target_pocket=PackagePublishingPocket.RELEASE,
463 include_binaries=False)
464- self.assertRaises(CannotCopy, self.runJob, job)
465+ naked_job = removeSecurityProxy(job)
466+ naked_job.reportFailure = FakeMethod()
467+
468+ job.run()
469+
470+ self.assertEqual(1, naked_job.reportFailure.call_count)
471
472 def test_target_ppa_non_release_pocket(self):
473 # When copying to a PPA archive the target must be the release pocket.
474@@ -134,7 +189,13 @@
475 target_archive=archive2, target_distroseries=distroseries,
476 target_pocket=PackagePublishingPocket.UPDATES,
477 include_binaries=False)
478- self.assertRaises(CannotCopy, self.runJob, job)
479+
480+ naked_job = removeSecurityProxy(job)
481+ naked_job.reportFailure = FakeMethod()
482+
483+ job.run()
484+
485+ self.assertEqual(1, naked_job.reportFailure.call_count)
486
487 def test_run(self):
488 # A proper test run synchronizes packages.
489@@ -164,8 +225,9 @@
490
491 source = getUtility(IPlainPackageCopyJobSource)
492 job = source.create(
493- source_packages=[("libc", "2.8-1")], source_archive=breezy_archive,
494- target_archive=target_archive, target_distroseries=target_series,
495+ source_packages=[("libc", "2.8-1")],
496+ source_archive=breezy_archive, target_archive=target_archive,
497+ target_distroseries=target_series,
498 target_pocket=PackagePublishingPocket.RELEASE,
499 include_binaries=False)
500 self.assertContentEqual(
501@@ -275,8 +337,7 @@
502 def test_getPendingJobsPerPackage_ignores_other_distroseries(self):
503 # getPendingJobsPerPackage only looks for jobs on the indicated
504 # distroseries.
505- dsd = self.factory.makeDistroSeriesDifference()
506- self.makeJob(dsd)
507+ self.makeJob()
508 other_series = self.factory.makeDistroSeries()
509 job_source = getUtility(IPlainPackageCopyJobSource)
510 self.assertEqual(
511@@ -333,3 +394,48 @@
512 job_source = getUtility(IPlainPackageCopyJobSource)
513 self.assertEqual(
514 {}, job_source.getPendingJobsPerPackage(dsd.derived_series))
515+
516+ def test_findMatchingDSDs_matches_all_DSDs_for_job(self):
517+ # findMatchingDSDs finds matching DSDs for any of the packages
518+ # in the job.
519+ dsd = self.factory.makeDistroSeriesDifference()
520+ naked_job = removeSecurityProxy(self.makeJob(dsd))
521+ self.assertContentEqual([dsd], naked_job.findMatchingDSDs())
522+
523+ def test_findMatchingDSDs_ignores_other_source_series(self):
524+ # findMatchingDSDs tries to ignore DSDs that are for different
525+ # parent series than the job's source series. (This can't be
526+ # done with perfect precision because the job doesn't keep track
527+ # of source distroseries, but in practice it should be good
528+ # enough).
529+ dsd = self.factory.makeDistroSeriesDifference()
530+ naked_job = removeSecurityProxy(self.makeJob(dsd))
531+
532+ # If the dsd differs only in parent series, that's enough to
533+ # make it a non-match.
534+ removeSecurityProxy(dsd).parent_series = (
535+ self.factory.makeDistroSeries())
536+
537+ self.assertContentEqual([], naked_job.findMatchingDSDs())
538+
539+
540+class TestPlainPackageCopyJobPrivileges(TestCaseWithFactory, LocalTestHelper):
541+ """Test that `PlainPackageCopyJob` has the privileges it needs.
542+
543+ This test looks for errors, not failures. It's here only to see that
544+ these operations don't run into any privilege limitations.
545+ """
546+
547+ layer = LaunchpadZopelessLayer
548+
549+ def test_findMatchingDSDs(self):
550+ job = self.makeJob()
551+ transaction.commit()
552+ self.layer.switchDbUser(config.IPlainPackageCopyJobSource.dbuser)
553+ removeSecurityProxy(job).findMatchingDSDs()
554+
555+ def test_reportFailure(self):
556+ job = self.makeJob()
557+ transaction.commit()
558+ self.layer.switchDbUser(config.IPlainPackageCopyJobSource.dbuser)
559+ removeSecurityProxy(job).reportFailure(CannotCopy("Mommy it hurts"))

Subscribers

People subscribed via source and target branches

to status/vote changes: