Merge lp:~cjwatson/launchpad/snap-store-upload-job into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18032
Proposed branch: lp:~cjwatson/launchpad/snap-store-upload-job
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/snap-store-client
Diff against target: 632 lines (+478/-2)
11 files modified
database/schema/security.cfg (+18/-0)
lib/lp/archiveuploader/tests/test_snapupload.py (+28/-0)
lib/lp/services/config/schema-lazr.conf (+5/-0)
lib/lp/snappy/configure.zcml (+14/-0)
lib/lp/snappy/interfaces/snapbuild.py (+10/-1)
lib/lp/snappy/interfaces/snapbuildjob.py (+58/-0)
lib/lp/snappy/model/snapbuild.py (+19/-0)
lib/lp/snappy/model/snapbuildjob.py (+191/-0)
lib/lp/snappy/subscribers/snapbuild.py (+7/-1)
lib/lp/snappy/tests/test_snapbuild.py (+23/-0)
lib/lp/snappy/tests/test_snapbuildjob.py (+105/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-store-upload-job
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+294031@code.launchpad.net

Commit message

Add a job to upload completed snap builds to the store.

Description of the change

Add a job to upload completed snap builds to the store.

We'll need to create a snap-build-job DB user before landing this.

To post a comment you must log in.
William Grant (wgrant) :
review: Approve (code)
Colin Watson (cjwatson) :

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 2016-05-06 14:39:05 +0000
3+++ database/schema/security.cfg 2016-05-12 15:17:24 +0000
4@@ -1431,7 +1431,9 @@
5 public.snap = SELECT, UPDATE
6 public.snaparch = SELECT
7 public.snapbuild = SELECT, UPDATE
8+public.snapbuildjob = SELECT, INSERT, UPDATE
9 public.snapfile = SELECT, INSERT, UPDATE
10+public.snappyseries = SELECT
11 public.sourcepackagefilepublishing = SELECT
12 public.sourcepackageformatselection = SELECT
13 public.sourcepackagename = SELECT, INSERT
14@@ -2523,3 +2525,19 @@
15 public.sourcepackagename = SELECT
16 public.webhook = SELECT
17 public.webhookjob = SELECT, UPDATE
18+
19+[snap-build-job]
20+type=user
21+groups=script
22+public.distribution = SELECT
23+public.distroarchseries = SELECT
24+public.distroseries = SELECT
25+public.job = SELECT, UPDATE
26+public.libraryfilealias = SELECT
27+public.libraryfilecontent = SELECT
28+public.person = SELECT
29+public.snap = SELECT
30+public.snapbuild = SELECT, UPDATE
31+public.snapbuildjob = SELECT, UPDATE
32+public.snapfile = SELECT
33+public.snappyseries = SELECT
34
35=== modified file 'lib/lp/archiveuploader/tests/test_snapupload.py'
36--- lib/lp/archiveuploader/tests/test_snapupload.py 2015-08-03 15:07:29 +0000
37+++ lib/lp/archiveuploader/tests/test_snapupload.py 2016-05-12 15:17:24 +0000
38@@ -64,3 +64,31 @@
39 "Snap upload failed\nGot: %s" % self.log.getLogBuffer())
40 self.assertEqual(BuildStatus.FULLYBUILT, self.build.status)
41 self.assertTrue(self.build.verifySuccessfulUpload())
42+
43+ def test_triggers_store_uploads(self):
44+ # The upload processor triggers store uploads if appropriate.
45+ self.pushConfig(
46+ "snappy", store_url="http://sca.example/",
47+ store_upload_url="http://updown.example/")
48+ self.switchToAdmin()
49+ self.snap.store_series = self.factory.makeSnappySeries(
50+ usable_distro_series=[self.snap.distro_series])
51+ self.snap.store_name = self.snap.name
52+ self.snap.store_upload = True
53+ self.snap.store_secrets = {
54+ "root": "dummy-root", "discharge": "dummy-discharge"}
55+ Store.of(self.snap).flush()
56+ self.switchToUploader()
57+ self.assertFalse(self.build.verifySuccessfulUpload())
58+ upload_dir = os.path.join(
59+ self.incoming_folder, "test", str(self.build.id), "ubuntu")
60+ write_file(os.path.join(upload_dir, "wget_0_all.snap"), "snap")
61+ handler = UploadHandler.forProcessor(
62+ self.uploadprocessor, self.incoming_folder, "test", self.build)
63+ result = handler.processSnap(self.log)
64+ self.assertEqual(
65+ UploadStatusEnum.ACCEPTED, result,
66+ "Snap upload failed\nGot: %s" % self.log.getLogBuffer())
67+ self.assertEqual(BuildStatus.FULLYBUILT, self.build.status)
68+ self.assertTrue(self.build.verifySuccessfulUpload())
69+ self.assertEqual(1, len(list(self.build.store_upload_jobs)))
70
71=== modified file 'lib/lp/services/config/schema-lazr.conf'
72--- lib/lp/services/config/schema-lazr.conf 2016-05-03 16:38:52 +0000
73+++ lib/lp/services/config/schema-lazr.conf 2016-05-12 15:17:24 +0000
74@@ -1807,6 +1807,7 @@
75 IRemoveArtifactSubscriptionsJobSource,
76 ISelfRenewalNotificationJobSource,
77 ISevenDayCommercialExpirationJobSource,
78+ ISnapStoreUploadJobSource,
79 ITeamInvitationNotificationJobSource,
80 ITeamJoinNotificationJobSource,
81 IThirtyDayCommercialExpirationJobSource
82@@ -1948,6 +1949,10 @@
83 dbuser: product-job
84 crontab_group: MAIN
85
86+[ISnapStoreUploadJobSource]
87+module: lp.snappy.interfaces.snapbuildjob
88+dbuser: snap-build-job
89+
90 [ITeamInvitationNotificationJobSource]
91 module: lp.registry.interfaces.persontransferjob
92 dbuser: person-transfer-job
93
94=== modified file 'lib/lp/snappy/configure.zcml'
95--- lib/lp/snappy/configure.zcml 2016-05-06 13:14:32 +0000
96+++ lib/lp/snappy/configure.zcml 2016-05-12 15:17:24 +0000
97@@ -124,6 +124,20 @@
98 <allow interface="lp.snappy.interfaces.snapstoreclient.ISnapStoreClient" />
99 </securedutility>
100
101+ <!-- Snap-related jobs -->
102+ <class class="lp.snappy.model.snapbuildjob.SnapBuildJob">
103+ <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapBuildJob" />
104+ </class>
105+ <securedutility
106+ component="lp.snappy.model.snapbuildjob.SnapStoreUploadJob"
107+ provides="lp.snappy.interfaces.snapbuildjob.ISnapStoreUploadJobSource">
108+ <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapStoreUploadJobSource" />
109+ </securedutility>
110+ <class class="lp.snappy.model.snapbuildjob.SnapStoreUploadJob">
111+ <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapBuildJob" />
112+ <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapStoreUploadJob" />
113+ </class>
114+
115 <webservice:register module="lp.snappy.interfaces.webservice" />
116
117 </configure>
118
119=== modified file 'lib/lp/snappy/interfaces/snapbuild.py'
120--- lib/lp/snappy/interfaces/snapbuild.py 2016-05-04 15:20:26 +0000
121+++ lib/lp/snappy/interfaces/snapbuild.py 2016-05-12 15:17:24 +0000
122@@ -20,7 +20,10 @@
123 operation_for_version,
124 operation_parameters,
125 )
126-from lazr.restful.fields import Reference
127+from lazr.restful.fields import (
128+ CollectionField,
129+ Reference,
130+ )
131 from zope.component.interfaces import IObjectEvent
132 from zope.interface import Interface
133 from zope.schema import (
134@@ -103,6 +106,12 @@
135 required=True, readonly=True,
136 description=_("Whether this build record can be cancelled.")))
137
138+ store_upload_jobs = CollectionField(
139+ title=_("Store upload jobs for this build."),
140+ # Really ISnapStoreUploadJob.
141+ value_type=Reference(schema=Interface),
142+ readonly=True)
143+
144 def getFiles():
145 """Retrieve the build's `ISnapFile` records.
146
147
148=== added file 'lib/lp/snappy/interfaces/snapbuildjob.py'
149--- lib/lp/snappy/interfaces/snapbuildjob.py 1970-01-01 00:00:00 +0000
150+++ lib/lp/snappy/interfaces/snapbuildjob.py 2016-05-12 15:17:24 +0000
151@@ -0,0 +1,58 @@
152+# Copyright 2016 Canonical Ltd. This software is licensed under the
153+# GNU Affero General Public License version 3 (see the file LICENSE).
154+
155+"""Snap build job interfaces."""
156+
157+from __future__ import absolute_import, print_function, unicode_literals
158+
159+__metaclass__ = type
160+__all__ = [
161+ 'ISnapBuildJob',
162+ 'ISnapStoreUploadJob',
163+ 'ISnapStoreUploadJobSource',
164+ ]
165+
166+from lazr.restful.fields import Reference
167+from zope.interface import (
168+ Attribute,
169+ Interface,
170+ )
171+from zope.schema import TextLine
172+
173+from lp import _
174+from lp.services.job.interfaces.job import (
175+ IJob,
176+ IJobSource,
177+ IRunnableJob,
178+ )
179+from lp.snappy.interfaces.snapbuild import ISnapBuild
180+
181+
182+class ISnapBuildJob(Interface):
183+ """A job related to a snap package."""
184+
185+ job = Reference(
186+ title=_("The common Job attributes."), schema=IJob,
187+ required=True, readonly=True)
188+
189+ snapbuild = Reference(
190+ title=_("The snap build to use for this job."),
191+ schema=ISnapBuild, required=True, readonly=True)
192+
193+ metadata = Attribute(_("A dict of data about the job."))
194+
195+
196+class ISnapStoreUploadJob(IRunnableJob):
197+ """A Job that uploads a snap build to the store."""
198+
199+ error_message = TextLine(
200+ title=_("Error message"), required=False, readonly=True)
201+
202+
203+class ISnapStoreUploadJobSource(IJobSource):
204+
205+ def create(snapbuild):
206+ """Upload a snap build to the store.
207+
208+ :param snapbuild: The snap build to upload.
209+ """
210
211=== modified file 'lib/lp/snappy/model/snapbuild.py'
212--- lib/lp/snappy/model/snapbuild.py 2016-05-04 15:20:26 +0000
213+++ lib/lp/snappy/model/snapbuild.py 2016-05-12 15:17:24 +0000
214@@ -48,6 +48,7 @@
215 IStore,
216 )
217 from lp.services.features import getFeatureFlag
218+from lp.services.job.model.job import Job
219 from lp.services.librarian.browser import ProxiedLibraryFileAlias
220 from lp.services.librarian.model import (
221 LibraryFileAlias,
222@@ -65,6 +66,10 @@
223 ISnapFile,
224 )
225 from lp.snappy.mail.snapbuild import SnapBuildMailer
226+from lp.snappy.model.snapbuildjob import (
227+ SnapBuildJob,
228+ SnapBuildJobType,
229+ )
230 from lp.soyuz.interfaces.component import IComponentSet
231 from lp.soyuz.model.archive import Archive
232 from lp.soyuz.model.distroarchseries import DistroArchSeries
233@@ -346,6 +351,20 @@
234 def getFileUrls(self):
235 return [self.lfaUrl(lfa) for _, lfa, _ in self.getFiles()]
236
237+ @property
238+ def store_upload_jobs(self):
239+ jobs = Store.of(self).find(
240+ SnapBuildJob,
241+ SnapBuildJob.snapbuild == self,
242+ SnapBuildJob.job_type == SnapBuildJobType.STORE_UPLOAD)
243+ jobs.order_by(Desc(SnapBuildJob.job_id))
244+
245+ def preload_jobs(rows):
246+ load_related(Job, rows, ["job_id"])
247+
248+ return DecoratedResultSet(
249+ jobs, lambda job: job.makeDerived(), pre_iter_hook=preload_jobs)
250+
251
252 @implementer(ISnapBuildSet)
253 class SnapBuildSet(SpecificBuildFarmJobSourceMixin):
254
255=== added file 'lib/lp/snappy/model/snapbuildjob.py'
256--- lib/lp/snappy/model/snapbuildjob.py 1970-01-01 00:00:00 +0000
257+++ lib/lp/snappy/model/snapbuildjob.py 2016-05-12 15:17:24 +0000
258@@ -0,0 +1,191 @@
259+# Copyright 2016 Canonical Ltd. This software is licensed under the
260+# GNU Affero General Public License version 3 (see the file LICENSE).
261+
262+"""Snap build jobs."""
263+
264+from __future__ import absolute_import, print_function, unicode_literals
265+
266+__metaclass__ = type
267+__all__ = [
268+ 'SnapBuildJob',
269+ 'SnapBuildJobType',
270+ 'SnapStoreUploadJob',
271+ ]
272+
273+from lazr.delegates import delegate_to
274+from lazr.enum import (
275+ DBEnumeratedType,
276+ DBItem,
277+ )
278+from storm.locals import (
279+ Int,
280+ JSON,
281+ Reference,
282+ )
283+import transaction
284+from zope.component import getUtility
285+from zope.interface import (
286+ implementer,
287+ provider,
288+ )
289+
290+from lp.app.errors import NotFoundError
291+from lp.services.config import config
292+from lp.services.database.enumcol import EnumCol
293+from lp.services.database.interfaces import (
294+ IMasterStore,
295+ IStore,
296+ )
297+from lp.services.database.stormbase import StormBase
298+from lp.services.job.model.job import (
299+ EnumeratedSubclass,
300+ Job,
301+ )
302+from lp.services.job.runner import BaseRunnableJob
303+from lp.snappy.interfaces.snapbuildjob import (
304+ ISnapBuildJob,
305+ ISnapStoreUploadJob,
306+ ISnapStoreUploadJobSource,
307+ )
308+from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
309+
310+
311+class SnapBuildJobType(DBEnumeratedType):
312+ """Values that `ISnapBuildJob.job_type` can take."""
313+
314+ STORE_UPLOAD = DBItem(0, """
315+ Store upload
316+
317+ This job uploads a snap build to the store.
318+ """)
319+
320+
321+@implementer(ISnapBuildJob)
322+class SnapBuildJob(StormBase):
323+ """See `ISnapBuildJob`."""
324+
325+ __storm_table__ = 'SnapBuildJob'
326+
327+ job_id = Int(name='job', primary=True, allow_none=False)
328+ job = Reference(job_id, 'Job.id')
329+
330+ snapbuild_id = Int(name='snapbuild', allow_none=False)
331+ snapbuild = Reference(snapbuild_id, 'SnapBuild.id')
332+
333+ job_type = EnumCol(enum=SnapBuildJobType, notNull=True)
334+
335+ metadata = JSON('json_data', allow_none=False)
336+
337+ def __init__(self, snapbuild, job_type, metadata, **job_args):
338+ """Constructor.
339+
340+ Extra keyword arguments are used to construct the underlying Job
341+ object.
342+
343+ :param snapbuild: The `ISnapBuild` this job relates to.
344+ :param job_type: The `SnapBuildJobType` of this job.
345+ :param metadata: The type-specific variables, as a JSON-compatible
346+ dict.
347+ """
348+ super(SnapBuildJob, self).__init__()
349+ self.job = Job(**job_args)
350+ self.snapbuild = snapbuild
351+ self.job_type = job_type
352+ self.metadata = metadata
353+
354+ def makeDerived(self):
355+ return SnapBuildJobDerived.makeSubclass(self)
356+
357+
358+@delegate_to(ISnapBuildJob)
359+class SnapBuildJobDerived(BaseRunnableJob):
360+
361+ __metaclass__ = EnumeratedSubclass
362+
363+ def __init__(self, snap_build_job):
364+ self.context = snap_build_job
365+
366+ def __repr__(self):
367+ """An informative representation of the job."""
368+ return "<%s for %s>" % (self.__class__.__name__, self.snapbuild.title)
369+
370+ @classmethod
371+ def get(cls, job_id):
372+ """Get a job by id.
373+
374+ :return: The `SnapBuildJob` with the specified id, as the current
375+ `SnapBuildJobDerived` subclass.
376+ :raises: `NotFoundError` if there is no job with the specified id,
377+ or its `job_type` does not match the desired subclass.
378+ """
379+ snap_build_job = IStore(SnapBuildJob).get(SnapBuildJob, job_id)
380+ if snap_build_job.job_type != cls.class_job_type:
381+ raise NotFoundError(
382+ "No object found with id %d and type %s" %
383+ (job_id, cls.class_job_type.title))
384+ return cls(snap_build_job)
385+
386+ @classmethod
387+ def iterReady(cls):
388+ """See `IJobSource`."""
389+ jobs = IMasterStore(SnapBuildJob).find(
390+ SnapBuildJob,
391+ SnapBuildJob.job_type == cls.class_job_type,
392+ SnapBuildJob.job == Job.id,
393+ Job.id.is_in(Job.ready_jobs))
394+ return (cls(job) for job in jobs)
395+
396+ def getOopsVars(self):
397+ """See `IRunnableJob`."""
398+ oops_vars = super(SnapBuildJobDerived, self).getOopsVars()
399+ oops_vars.extend([
400+ ('job_id', self.context.job.id),
401+ ('job_type', self.context.job_type.title),
402+ ('snapbuild_id', self.context.snapbuild.id),
403+ ('snap_owner_name', self.context.snapbuild.snap.owner.name),
404+ ('snap_name', self.context.snapbuild.snap.name),
405+ ])
406+ return oops_vars
407+
408+
409+@implementer(ISnapStoreUploadJob)
410+@provider(ISnapStoreUploadJobSource)
411+class SnapStoreUploadJob(SnapBuildJobDerived):
412+ """A Job that uploads a snap build to the store."""
413+
414+ class_job_type = SnapBuildJobType.STORE_UPLOAD
415+
416+ # XXX cjwatson 2016-05-04: identify transient upload failures and retry
417+
418+ config = config.ISnapStoreUploadJobSource
419+
420+ @classmethod
421+ def create(cls, snapbuild):
422+ """See `ISnapStoreUploadJobSource`."""
423+ snap_build_job = SnapBuildJob(snapbuild, cls.class_job_type, {})
424+ job = cls(snap_build_job)
425+ job.celeryRunOnCommit()
426+ return job
427+
428+ @property
429+ def error_message(self):
430+ """See `ISnapStoreUploadJob`."""
431+ return self.metadata.get("error_message")
432+
433+ @error_message.setter
434+ def error_message(self, message):
435+ """See `ISnapStoreUploadJob`."""
436+ self.metadata["error_message"] = message
437+
438+ def run(self):
439+ """See `IRunnableJob`."""
440+ try:
441+ getUtility(ISnapStoreClient).upload(self.snapbuild)
442+ self.error_message = None
443+ except Exception as e:
444+ # Abort work done so far, but make sure that we commit the error
445+ # message.
446+ transaction.abort()
447+ self.error_message = str(e)
448+ transaction.commit()
449+ raise
450
451=== modified file 'lib/lp/snappy/subscribers/snapbuild.py'
452--- lib/lp/snappy/subscribers/snapbuild.py 2016-01-19 17:41:11 +0000
453+++ lib/lp/snappy/subscribers/snapbuild.py 2016-05-12 15:17:24 +0000
454@@ -9,16 +9,18 @@
455
456 from zope.component import getUtility
457
458+from lp.buildmaster.enums import BuildStatus
459 from lp.services.features import getFeatureFlag
460 from lp.services.webapp.publisher import canonical_url
461 from lp.services.webhooks.interfaces import IWebhookSet
462 from lp.services.webhooks.payload import compose_webhook_payload
463 from lp.snappy.interfaces.snap import SNAP_WEBHOOKS_FEATURE_FLAG
464 from lp.snappy.interfaces.snapbuild import ISnapBuild
465+from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource
466
467
468 def snap_build_status_changed(snapbuild, event):
469- """Trigger webhooks when snap package build statuses change."""
470+ """Trigger events when snap package build statuses change."""
471 if getFeatureFlag(SNAP_WEBHOOKS_FEATURE_FLAG):
472 payload = {
473 "snap_build": canonical_url(snapbuild, force_local_path=True),
474@@ -28,3 +30,7 @@
475 ISnapBuild, snapbuild, ["snap", "status"]))
476 getUtility(IWebhookSet).trigger(
477 snapbuild.snap, "snap:build:0.1", payload)
478+
479+ if (snapbuild.snap.can_upload_to_store and
480+ snapbuild.status == BuildStatus.FULLYBUILT):
481+ getUtility(ISnapStoreUploadJobSource).create(snapbuild)
482
483=== modified file 'lib/lp/snappy/tests/test_snapbuild.py'
484--- lib/lp/snappy/tests/test_snapbuild.py 2016-03-02 21:21:26 +0000
485+++ lib/lp/snappy/tests/test_snapbuild.py 2016-05-12 15:17:24 +0000
486@@ -97,6 +97,9 @@
487 def setUp(self):
488 super(TestSnapBuild, self).setUp()
489 self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
490+ self.pushConfig(
491+ "snappy", store_url="http://sca.example/",
492+ store_upload_url="http://updown.example/")
493 self.build = self.factory.makeSnapBuild()
494
495 def test_implements_interfaces(self):
496@@ -250,6 +253,26 @@
497 hook.id, hook.target),
498 repr(delivery))
499
500+ def test_updateStatus_failure_does_not_trigger_store_uploads(self):
501+ # A failed SnapBuild does not trigger store uploads.
502+ self.build.snap.store_series = self.factory.makeSnappySeries()
503+ self.build.snap.store_name = self.factory.getUniqueUnicode()
504+ self.build.snap.store_upload = True
505+ self.build.snap.store_secrets = {
506+ "root": "dummy-root", "discharge": "dummy-discharge"}
507+ self.build.updateStatus(BuildStatus.FAILEDTOBUILD)
508+ self.assertContentEqual([], self.build.store_upload_jobs)
509+
510+ def test_updateStatus_fullybuilt_triggers_store_uploads(self):
511+ # A completed SnapBuild triggers store uploads.
512+ self.build.snap.store_series = self.factory.makeSnappySeries()
513+ self.build.snap.store_name = self.factory.getUniqueUnicode()
514+ self.build.snap.store_upload = True
515+ self.build.snap.store_secrets = {
516+ "root": "dummy-root", "discharge": "dummy-discharge"}
517+ self.build.updateStatus(BuildStatus.FULLYBUILT)
518+ self.assertEqual(1, len(list(self.build.store_upload_jobs)))
519+
520 def test_notify_fullybuilt(self):
521 # notify does not send mail when a SnapBuild completes normally.
522 person = self.factory.makePerson(name="person")
523
524=== added file 'lib/lp/snappy/tests/test_snapbuildjob.py'
525--- lib/lp/snappy/tests/test_snapbuildjob.py 1970-01-01 00:00:00 +0000
526+++ lib/lp/snappy/tests/test_snapbuildjob.py 2016-05-12 15:17:24 +0000
527@@ -0,0 +1,105 @@
528+# Copyright 2016 Canonical Ltd. This software is licensed under the
529+# GNU Affero General Public License version 3 (see the file LICENSE).
530+
531+"""Tests for snap build jobs."""
532+
533+from __future__ import absolute_import, print_function, unicode_literals
534+
535+__metaclass__ = type
536+
537+from zope.interface import implementer
538+
539+from lp.services.config import config
540+from lp.services.features.testing import FeatureFixture
541+from lp.services.job.runner import JobRunner
542+from lp.snappy.interfaces.snap import SNAP_TESTING_FLAGS
543+from lp.snappy.interfaces.snapbuildjob import (
544+ ISnapBuildJob,
545+ ISnapStoreUploadJob,
546+ )
547+from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
548+from lp.snappy.model.snapbuildjob import (
549+ SnapBuildJob,
550+ SnapBuildJobType,
551+ SnapStoreUploadJob,
552+ )
553+from lp.testing import TestCaseWithFactory
554+from lp.testing.dbuser import dbuser
555+from lp.testing.fakemethod import FakeMethod
556+from lp.testing.fixture import ZopeUtilityFixture
557+from lp.testing.layers import (
558+ DatabaseFunctionalLayer,
559+ LaunchpadZopelessLayer,
560+ )
561+
562+
563+@implementer(ISnapStoreClient)
564+class FakeSnapStoreClient:
565+
566+ def __init__(self):
567+ self.upload = FakeMethod()
568+
569+
570+class TestSnapBuildJob(TestCaseWithFactory):
571+
572+ layer = DatabaseFunctionalLayer
573+
574+ def setUp(self):
575+ super(TestSnapBuildJob, self).setUp()
576+ self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
577+
578+ def test_provides_interface(self):
579+ # `SnapBuildJob` objects provide `ISnapBuildJob`.
580+ snapbuild = self.factory.makeSnapBuild()
581+ self.assertProvides(
582+ SnapBuildJob(snapbuild, SnapBuildJobType.STORE_UPLOAD, {}),
583+ ISnapBuildJob)
584+
585+
586+class TestSnapStoreUploadJob(TestCaseWithFactory):
587+
588+ layer = LaunchpadZopelessLayer
589+
590+ def setUp(self):
591+ super(TestSnapStoreUploadJob, self).setUp()
592+ self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
593+
594+ def test_provides_interface(self):
595+ # `SnapStoreUploadJob` objects provide `ISnapStoreUploadJob`.
596+ snapbuild = self.factory.makeSnapBuild()
597+ job = SnapStoreUploadJob.create(snapbuild)
598+ self.assertProvides(job, ISnapStoreUploadJob)
599+
600+ def test___repr__(self):
601+ # `SnapStoreUploadJob` objects have an informative __repr__.
602+ snapbuild = self.factory.makeSnapBuild()
603+ job = SnapStoreUploadJob.create(snapbuild)
604+ self.assertEqual(
605+ "<SnapStoreUploadJob for %s>" % snapbuild.title, repr(job))
606+
607+ def test_run(self):
608+ # The job uploads the build to the store.
609+ snapbuild = self.factory.makeSnapBuild()
610+ self.assertContentEqual([], snapbuild.store_upload_jobs)
611+ job = SnapStoreUploadJob.create(snapbuild)
612+ client = FakeSnapStoreClient()
613+ self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
614+ with dbuser(config.ISnapStoreUploadJobSource.dbuser):
615+ JobRunner([job]).runAll()
616+ self.assertEqual([((snapbuild,), {})], client.upload.calls)
617+ self.assertContentEqual([job], snapbuild.store_upload_jobs)
618+ self.assertIsNone(job.error_message)
619+
620+ def test_run_failed(self):
621+ # A failed run sets the store upload status to FAILED.
622+ snapbuild = self.factory.makeSnapBuild()
623+ self.assertContentEqual([], snapbuild.store_upload_jobs)
624+ job = SnapStoreUploadJob.create(snapbuild)
625+ client = FakeSnapStoreClient()
626+ client.upload.failure = ValueError("An upload failure")
627+ self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
628+ with dbuser(config.ISnapStoreUploadJobSource.dbuser):
629+ JobRunner([job]).runAll()
630+ self.assertEqual([((snapbuild,), {})], client.upload.calls)
631+ self.assertContentEqual([job], snapbuild.store_upload_jobs)
632+ self.assertEqual("An upload failure", job.error_message)