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.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2016-05-06 14:39:05 +0000
+++ database/schema/security.cfg 2016-05-12 15:17:24 +0000
@@ -1431,7 +1431,9 @@
1431public.snap = SELECT, UPDATE1431public.snap = SELECT, UPDATE
1432public.snaparch = SELECT1432public.snaparch = SELECT
1433public.snapbuild = SELECT, UPDATE1433public.snapbuild = SELECT, UPDATE
1434public.snapbuildjob = SELECT, INSERT, UPDATE
1434public.snapfile = SELECT, INSERT, UPDATE1435public.snapfile = SELECT, INSERT, UPDATE
1436public.snappyseries = SELECT
1435public.sourcepackagefilepublishing = SELECT1437public.sourcepackagefilepublishing = SELECT
1436public.sourcepackageformatselection = SELECT1438public.sourcepackageformatselection = SELECT
1437public.sourcepackagename = SELECT, INSERT1439public.sourcepackagename = SELECT, INSERT
@@ -2523,3 +2525,19 @@
2523public.sourcepackagename = SELECT2525public.sourcepackagename = SELECT
2524public.webhook = SELECT2526public.webhook = SELECT
2525public.webhookjob = SELECT, UPDATE2527public.webhookjob = SELECT, UPDATE
2528
2529[snap-build-job]
2530type=user
2531groups=script
2532public.distribution = SELECT
2533public.distroarchseries = SELECT
2534public.distroseries = SELECT
2535public.job = SELECT, UPDATE
2536public.libraryfilealias = SELECT
2537public.libraryfilecontent = SELECT
2538public.person = SELECT
2539public.snap = SELECT
2540public.snapbuild = SELECT, UPDATE
2541public.snapbuildjob = SELECT, UPDATE
2542public.snapfile = SELECT
2543public.snappyseries = SELECT
25262544
=== modified file 'lib/lp/archiveuploader/tests/test_snapupload.py'
--- lib/lp/archiveuploader/tests/test_snapupload.py 2015-08-03 15:07:29 +0000
+++ lib/lp/archiveuploader/tests/test_snapupload.py 2016-05-12 15:17:24 +0000
@@ -64,3 +64,31 @@
64 "Snap upload failed\nGot: %s" % self.log.getLogBuffer())64 "Snap upload failed\nGot: %s" % self.log.getLogBuffer())
65 self.assertEqual(BuildStatus.FULLYBUILT, self.build.status)65 self.assertEqual(BuildStatus.FULLYBUILT, self.build.status)
66 self.assertTrue(self.build.verifySuccessfulUpload())66 self.assertTrue(self.build.verifySuccessfulUpload())
67
68 def test_triggers_store_uploads(self):
69 # The upload processor triggers store uploads if appropriate.
70 self.pushConfig(
71 "snappy", store_url="http://sca.example/",
72 store_upload_url="http://updown.example/")
73 self.switchToAdmin()
74 self.snap.store_series = self.factory.makeSnappySeries(
75 usable_distro_series=[self.snap.distro_series])
76 self.snap.store_name = self.snap.name
77 self.snap.store_upload = True
78 self.snap.store_secrets = {
79 "root": "dummy-root", "discharge": "dummy-discharge"}
80 Store.of(self.snap).flush()
81 self.switchToUploader()
82 self.assertFalse(self.build.verifySuccessfulUpload())
83 upload_dir = os.path.join(
84 self.incoming_folder, "test", str(self.build.id), "ubuntu")
85 write_file(os.path.join(upload_dir, "wget_0_all.snap"), "snap")
86 handler = UploadHandler.forProcessor(
87 self.uploadprocessor, self.incoming_folder, "test", self.build)
88 result = handler.processSnap(self.log)
89 self.assertEqual(
90 UploadStatusEnum.ACCEPTED, result,
91 "Snap upload failed\nGot: %s" % self.log.getLogBuffer())
92 self.assertEqual(BuildStatus.FULLYBUILT, self.build.status)
93 self.assertTrue(self.build.verifySuccessfulUpload())
94 self.assertEqual(1, len(list(self.build.store_upload_jobs)))
6795
=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf 2016-05-03 16:38:52 +0000
+++ lib/lp/services/config/schema-lazr.conf 2016-05-12 15:17:24 +0000
@@ -1807,6 +1807,7 @@
1807 IRemoveArtifactSubscriptionsJobSource,1807 IRemoveArtifactSubscriptionsJobSource,
1808 ISelfRenewalNotificationJobSource,1808 ISelfRenewalNotificationJobSource,
1809 ISevenDayCommercialExpirationJobSource,1809 ISevenDayCommercialExpirationJobSource,
1810 ISnapStoreUploadJobSource,
1810 ITeamInvitationNotificationJobSource,1811 ITeamInvitationNotificationJobSource,
1811 ITeamJoinNotificationJobSource,1812 ITeamJoinNotificationJobSource,
1812 IThirtyDayCommercialExpirationJobSource1813 IThirtyDayCommercialExpirationJobSource
@@ -1948,6 +1949,10 @@
1948dbuser: product-job1949dbuser: product-job
1949crontab_group: MAIN1950crontab_group: MAIN
19501951
1952[ISnapStoreUploadJobSource]
1953module: lp.snappy.interfaces.snapbuildjob
1954dbuser: snap-build-job
1955
1951[ITeamInvitationNotificationJobSource]1956[ITeamInvitationNotificationJobSource]
1952module: lp.registry.interfaces.persontransferjob1957module: lp.registry.interfaces.persontransferjob
1953dbuser: person-transfer-job1958dbuser: person-transfer-job
19541959
=== modified file 'lib/lp/snappy/configure.zcml'
--- lib/lp/snappy/configure.zcml 2016-05-06 13:14:32 +0000
+++ lib/lp/snappy/configure.zcml 2016-05-12 15:17:24 +0000
@@ -124,6 +124,20 @@
124 <allow interface="lp.snappy.interfaces.snapstoreclient.ISnapStoreClient" />124 <allow interface="lp.snappy.interfaces.snapstoreclient.ISnapStoreClient" />
125 </securedutility>125 </securedutility>
126126
127 <!-- Snap-related jobs -->
128 <class class="lp.snappy.model.snapbuildjob.SnapBuildJob">
129 <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapBuildJob" />
130 </class>
131 <securedutility
132 component="lp.snappy.model.snapbuildjob.SnapStoreUploadJob"
133 provides="lp.snappy.interfaces.snapbuildjob.ISnapStoreUploadJobSource">
134 <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapStoreUploadJobSource" />
135 </securedutility>
136 <class class="lp.snappy.model.snapbuildjob.SnapStoreUploadJob">
137 <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapBuildJob" />
138 <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapStoreUploadJob" />
139 </class>
140
127 <webservice:register module="lp.snappy.interfaces.webservice" />141 <webservice:register module="lp.snappy.interfaces.webservice" />
128142
129</configure>143</configure>
130144
=== modified file 'lib/lp/snappy/interfaces/snapbuild.py'
--- lib/lp/snappy/interfaces/snapbuild.py 2016-05-04 15:20:26 +0000
+++ lib/lp/snappy/interfaces/snapbuild.py 2016-05-12 15:17:24 +0000
@@ -20,7 +20,10 @@
20 operation_for_version,20 operation_for_version,
21 operation_parameters,21 operation_parameters,
22 )22 )
23from lazr.restful.fields import Reference23from lazr.restful.fields import (
24 CollectionField,
25 Reference,
26 )
24from zope.component.interfaces import IObjectEvent27from zope.component.interfaces import IObjectEvent
25from zope.interface import Interface28from zope.interface import Interface
26from zope.schema import (29from zope.schema import (
@@ -103,6 +106,12 @@
103 required=True, readonly=True,106 required=True, readonly=True,
104 description=_("Whether this build record can be cancelled.")))107 description=_("Whether this build record can be cancelled.")))
105108
109 store_upload_jobs = CollectionField(
110 title=_("Store upload jobs for this build."),
111 # Really ISnapStoreUploadJob.
112 value_type=Reference(schema=Interface),
113 readonly=True)
114
106 def getFiles():115 def getFiles():
107 """Retrieve the build's `ISnapFile` records.116 """Retrieve the build's `ISnapFile` records.
108117
109118
=== added file 'lib/lp/snappy/interfaces/snapbuildjob.py'
--- lib/lp/snappy/interfaces/snapbuildjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/interfaces/snapbuildjob.py 2016-05-12 15:17:24 +0000
@@ -0,0 +1,58 @@
1# Copyright 2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Snap build job interfaces."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9__all__ = [
10 'ISnapBuildJob',
11 'ISnapStoreUploadJob',
12 'ISnapStoreUploadJobSource',
13 ]
14
15from lazr.restful.fields import Reference
16from zope.interface import (
17 Attribute,
18 Interface,
19 )
20from zope.schema import TextLine
21
22from lp import _
23from lp.services.job.interfaces.job import (
24 IJob,
25 IJobSource,
26 IRunnableJob,
27 )
28from lp.snappy.interfaces.snapbuild import ISnapBuild
29
30
31class ISnapBuildJob(Interface):
32 """A job related to a snap package."""
33
34 job = Reference(
35 title=_("The common Job attributes."), schema=IJob,
36 required=True, readonly=True)
37
38 snapbuild = Reference(
39 title=_("The snap build to use for this job."),
40 schema=ISnapBuild, required=True, readonly=True)
41
42 metadata = Attribute(_("A dict of data about the job."))
43
44
45class ISnapStoreUploadJob(IRunnableJob):
46 """A Job that uploads a snap build to the store."""
47
48 error_message = TextLine(
49 title=_("Error message"), required=False, readonly=True)
50
51
52class ISnapStoreUploadJobSource(IJobSource):
53
54 def create(snapbuild):
55 """Upload a snap build to the store.
56
57 :param snapbuild: The snap build to upload.
58 """
059
=== modified file 'lib/lp/snappy/model/snapbuild.py'
--- lib/lp/snappy/model/snapbuild.py 2016-05-04 15:20:26 +0000
+++ lib/lp/snappy/model/snapbuild.py 2016-05-12 15:17:24 +0000
@@ -48,6 +48,7 @@
48 IStore,48 IStore,
49 )49 )
50from lp.services.features import getFeatureFlag50from lp.services.features import getFeatureFlag
51from lp.services.job.model.job import Job
51from lp.services.librarian.browser import ProxiedLibraryFileAlias52from lp.services.librarian.browser import ProxiedLibraryFileAlias
52from lp.services.librarian.model import (53from lp.services.librarian.model import (
53 LibraryFileAlias,54 LibraryFileAlias,
@@ -65,6 +66,10 @@
65 ISnapFile,66 ISnapFile,
66 )67 )
67from lp.snappy.mail.snapbuild import SnapBuildMailer68from lp.snappy.mail.snapbuild import SnapBuildMailer
69from lp.snappy.model.snapbuildjob import (
70 SnapBuildJob,
71 SnapBuildJobType,
72 )
68from lp.soyuz.interfaces.component import IComponentSet73from lp.soyuz.interfaces.component import IComponentSet
69from lp.soyuz.model.archive import Archive74from lp.soyuz.model.archive import Archive
70from lp.soyuz.model.distroarchseries import DistroArchSeries75from lp.soyuz.model.distroarchseries import DistroArchSeries
@@ -346,6 +351,20 @@
346 def getFileUrls(self):351 def getFileUrls(self):
347 return [self.lfaUrl(lfa) for _, lfa, _ in self.getFiles()]352 return [self.lfaUrl(lfa) for _, lfa, _ in self.getFiles()]
348353
354 @property
355 def store_upload_jobs(self):
356 jobs = Store.of(self).find(
357 SnapBuildJob,
358 SnapBuildJob.snapbuild == self,
359 SnapBuildJob.job_type == SnapBuildJobType.STORE_UPLOAD)
360 jobs.order_by(Desc(SnapBuildJob.job_id))
361
362 def preload_jobs(rows):
363 load_related(Job, rows, ["job_id"])
364
365 return DecoratedResultSet(
366 jobs, lambda job: job.makeDerived(), pre_iter_hook=preload_jobs)
367
349368
350@implementer(ISnapBuildSet)369@implementer(ISnapBuildSet)
351class SnapBuildSet(SpecificBuildFarmJobSourceMixin):370class SnapBuildSet(SpecificBuildFarmJobSourceMixin):
352371
=== added file 'lib/lp/snappy/model/snapbuildjob.py'
--- lib/lp/snappy/model/snapbuildjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/model/snapbuildjob.py 2016-05-12 15:17:24 +0000
@@ -0,0 +1,191 @@
1# Copyright 2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Snap build jobs."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9__all__ = [
10 'SnapBuildJob',
11 'SnapBuildJobType',
12 'SnapStoreUploadJob',
13 ]
14
15from lazr.delegates import delegate_to
16from lazr.enum import (
17 DBEnumeratedType,
18 DBItem,
19 )
20from storm.locals import (
21 Int,
22 JSON,
23 Reference,
24 )
25import transaction
26from zope.component import getUtility
27from zope.interface import (
28 implementer,
29 provider,
30 )
31
32from lp.app.errors import NotFoundError
33from lp.services.config import config
34from lp.services.database.enumcol import EnumCol
35from lp.services.database.interfaces import (
36 IMasterStore,
37 IStore,
38 )
39from lp.services.database.stormbase import StormBase
40from lp.services.job.model.job import (
41 EnumeratedSubclass,
42 Job,
43 )
44from lp.services.job.runner import BaseRunnableJob
45from lp.snappy.interfaces.snapbuildjob import (
46 ISnapBuildJob,
47 ISnapStoreUploadJob,
48 ISnapStoreUploadJobSource,
49 )
50from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
51
52
53class SnapBuildJobType(DBEnumeratedType):
54 """Values that `ISnapBuildJob.job_type` can take."""
55
56 STORE_UPLOAD = DBItem(0, """
57 Store upload
58
59 This job uploads a snap build to the store.
60 """)
61
62
63@implementer(ISnapBuildJob)
64class SnapBuildJob(StormBase):
65 """See `ISnapBuildJob`."""
66
67 __storm_table__ = 'SnapBuildJob'
68
69 job_id = Int(name='job', primary=True, allow_none=False)
70 job = Reference(job_id, 'Job.id')
71
72 snapbuild_id = Int(name='snapbuild', allow_none=False)
73 snapbuild = Reference(snapbuild_id, 'SnapBuild.id')
74
75 job_type = EnumCol(enum=SnapBuildJobType, notNull=True)
76
77 metadata = JSON('json_data', allow_none=False)
78
79 def __init__(self, snapbuild, job_type, metadata, **job_args):
80 """Constructor.
81
82 Extra keyword arguments are used to construct the underlying Job
83 object.
84
85 :param snapbuild: The `ISnapBuild` this job relates to.
86 :param job_type: The `SnapBuildJobType` of this job.
87 :param metadata: The type-specific variables, as a JSON-compatible
88 dict.
89 """
90 super(SnapBuildJob, self).__init__()
91 self.job = Job(**job_args)
92 self.snapbuild = snapbuild
93 self.job_type = job_type
94 self.metadata = metadata
95
96 def makeDerived(self):
97 return SnapBuildJobDerived.makeSubclass(self)
98
99
100@delegate_to(ISnapBuildJob)
101class SnapBuildJobDerived(BaseRunnableJob):
102
103 __metaclass__ = EnumeratedSubclass
104
105 def __init__(self, snap_build_job):
106 self.context = snap_build_job
107
108 def __repr__(self):
109 """An informative representation of the job."""
110 return "<%s for %s>" % (self.__class__.__name__, self.snapbuild.title)
111
112 @classmethod
113 def get(cls, job_id):
114 """Get a job by id.
115
116 :return: The `SnapBuildJob` with the specified id, as the current
117 `SnapBuildJobDerived` subclass.
118 :raises: `NotFoundError` if there is no job with the specified id,
119 or its `job_type` does not match the desired subclass.
120 """
121 snap_build_job = IStore(SnapBuildJob).get(SnapBuildJob, job_id)
122 if snap_build_job.job_type != cls.class_job_type:
123 raise NotFoundError(
124 "No object found with id %d and type %s" %
125 (job_id, cls.class_job_type.title))
126 return cls(snap_build_job)
127
128 @classmethod
129 def iterReady(cls):
130 """See `IJobSource`."""
131 jobs = IMasterStore(SnapBuildJob).find(
132 SnapBuildJob,
133 SnapBuildJob.job_type == cls.class_job_type,
134 SnapBuildJob.job == Job.id,
135 Job.id.is_in(Job.ready_jobs))
136 return (cls(job) for job in jobs)
137
138 def getOopsVars(self):
139 """See `IRunnableJob`."""
140 oops_vars = super(SnapBuildJobDerived, self).getOopsVars()
141 oops_vars.extend([
142 ('job_id', self.context.job.id),
143 ('job_type', self.context.job_type.title),
144 ('snapbuild_id', self.context.snapbuild.id),
145 ('snap_owner_name', self.context.snapbuild.snap.owner.name),
146 ('snap_name', self.context.snapbuild.snap.name),
147 ])
148 return oops_vars
149
150
151@implementer(ISnapStoreUploadJob)
152@provider(ISnapStoreUploadJobSource)
153class SnapStoreUploadJob(SnapBuildJobDerived):
154 """A Job that uploads a snap build to the store."""
155
156 class_job_type = SnapBuildJobType.STORE_UPLOAD
157
158 # XXX cjwatson 2016-05-04: identify transient upload failures and retry
159
160 config = config.ISnapStoreUploadJobSource
161
162 @classmethod
163 def create(cls, snapbuild):
164 """See `ISnapStoreUploadJobSource`."""
165 snap_build_job = SnapBuildJob(snapbuild, cls.class_job_type, {})
166 job = cls(snap_build_job)
167 job.celeryRunOnCommit()
168 return job
169
170 @property
171 def error_message(self):
172 """See `ISnapStoreUploadJob`."""
173 return self.metadata.get("error_message")
174
175 @error_message.setter
176 def error_message(self, message):
177 """See `ISnapStoreUploadJob`."""
178 self.metadata["error_message"] = message
179
180 def run(self):
181 """See `IRunnableJob`."""
182 try:
183 getUtility(ISnapStoreClient).upload(self.snapbuild)
184 self.error_message = None
185 except Exception as e:
186 # Abort work done so far, but make sure that we commit the error
187 # message.
188 transaction.abort()
189 self.error_message = str(e)
190 transaction.commit()
191 raise
0192
=== modified file 'lib/lp/snappy/subscribers/snapbuild.py'
--- lib/lp/snappy/subscribers/snapbuild.py 2016-01-19 17:41:11 +0000
+++ lib/lp/snappy/subscribers/snapbuild.py 2016-05-12 15:17:24 +0000
@@ -9,16 +9,18 @@
99
10from zope.component import getUtility10from zope.component import getUtility
1111
12from lp.buildmaster.enums import BuildStatus
12from lp.services.features import getFeatureFlag13from lp.services.features import getFeatureFlag
13from lp.services.webapp.publisher import canonical_url14from lp.services.webapp.publisher import canonical_url
14from lp.services.webhooks.interfaces import IWebhookSet15from lp.services.webhooks.interfaces import IWebhookSet
15from lp.services.webhooks.payload import compose_webhook_payload16from lp.services.webhooks.payload import compose_webhook_payload
16from lp.snappy.interfaces.snap import SNAP_WEBHOOKS_FEATURE_FLAG17from lp.snappy.interfaces.snap import SNAP_WEBHOOKS_FEATURE_FLAG
17from lp.snappy.interfaces.snapbuild import ISnapBuild18from lp.snappy.interfaces.snapbuild import ISnapBuild
19from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource
1820
1921
20def snap_build_status_changed(snapbuild, event):22def snap_build_status_changed(snapbuild, event):
21 """Trigger webhooks when snap package build statuses change."""23 """Trigger events when snap package build statuses change."""
22 if getFeatureFlag(SNAP_WEBHOOKS_FEATURE_FLAG):24 if getFeatureFlag(SNAP_WEBHOOKS_FEATURE_FLAG):
23 payload = {25 payload = {
24 "snap_build": canonical_url(snapbuild, force_local_path=True),26 "snap_build": canonical_url(snapbuild, force_local_path=True),
@@ -28,3 +30,7 @@
28 ISnapBuild, snapbuild, ["snap", "status"]))30 ISnapBuild, snapbuild, ["snap", "status"]))
29 getUtility(IWebhookSet).trigger(31 getUtility(IWebhookSet).trigger(
30 snapbuild.snap, "snap:build:0.1", payload)32 snapbuild.snap, "snap:build:0.1", payload)
33
34 if (snapbuild.snap.can_upload_to_store and
35 snapbuild.status == BuildStatus.FULLYBUILT):
36 getUtility(ISnapStoreUploadJobSource).create(snapbuild)
3137
=== modified file 'lib/lp/snappy/tests/test_snapbuild.py'
--- lib/lp/snappy/tests/test_snapbuild.py 2016-03-02 21:21:26 +0000
+++ lib/lp/snappy/tests/test_snapbuild.py 2016-05-12 15:17:24 +0000
@@ -97,6 +97,9 @@
97 def setUp(self):97 def setUp(self):
98 super(TestSnapBuild, self).setUp()98 super(TestSnapBuild, self).setUp()
99 self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))99 self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
100 self.pushConfig(
101 "snappy", store_url="http://sca.example/",
102 store_upload_url="http://updown.example/")
100 self.build = self.factory.makeSnapBuild()103 self.build = self.factory.makeSnapBuild()
101104
102 def test_implements_interfaces(self):105 def test_implements_interfaces(self):
@@ -250,6 +253,26 @@
250 hook.id, hook.target),253 hook.id, hook.target),
251 repr(delivery))254 repr(delivery))
252255
256 def test_updateStatus_failure_does_not_trigger_store_uploads(self):
257 # A failed SnapBuild does not trigger store uploads.
258 self.build.snap.store_series = self.factory.makeSnappySeries()
259 self.build.snap.store_name = self.factory.getUniqueUnicode()
260 self.build.snap.store_upload = True
261 self.build.snap.store_secrets = {
262 "root": "dummy-root", "discharge": "dummy-discharge"}
263 self.build.updateStatus(BuildStatus.FAILEDTOBUILD)
264 self.assertContentEqual([], self.build.store_upload_jobs)
265
266 def test_updateStatus_fullybuilt_triggers_store_uploads(self):
267 # A completed SnapBuild triggers store uploads.
268 self.build.snap.store_series = self.factory.makeSnappySeries()
269 self.build.snap.store_name = self.factory.getUniqueUnicode()
270 self.build.snap.store_upload = True
271 self.build.snap.store_secrets = {
272 "root": "dummy-root", "discharge": "dummy-discharge"}
273 self.build.updateStatus(BuildStatus.FULLYBUILT)
274 self.assertEqual(1, len(list(self.build.store_upload_jobs)))
275
253 def test_notify_fullybuilt(self):276 def test_notify_fullybuilt(self):
254 # notify does not send mail when a SnapBuild completes normally.277 # notify does not send mail when a SnapBuild completes normally.
255 person = self.factory.makePerson(name="person")278 person = self.factory.makePerson(name="person")
256279
=== added file 'lib/lp/snappy/tests/test_snapbuildjob.py'
--- lib/lp/snappy/tests/test_snapbuildjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/tests/test_snapbuildjob.py 2016-05-12 15:17:24 +0000
@@ -0,0 +1,105 @@
1# Copyright 2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for snap build jobs."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9
10from zope.interface import implementer
11
12from lp.services.config import config
13from lp.services.features.testing import FeatureFixture
14from lp.services.job.runner import JobRunner
15from lp.snappy.interfaces.snap import SNAP_TESTING_FLAGS
16from lp.snappy.interfaces.snapbuildjob import (
17 ISnapBuildJob,
18 ISnapStoreUploadJob,
19 )
20from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
21from lp.snappy.model.snapbuildjob import (
22 SnapBuildJob,
23 SnapBuildJobType,
24 SnapStoreUploadJob,
25 )
26from lp.testing import TestCaseWithFactory
27from lp.testing.dbuser import dbuser
28from lp.testing.fakemethod import FakeMethod
29from lp.testing.fixture import ZopeUtilityFixture
30from lp.testing.layers import (
31 DatabaseFunctionalLayer,
32 LaunchpadZopelessLayer,
33 )
34
35
36@implementer(ISnapStoreClient)
37class FakeSnapStoreClient:
38
39 def __init__(self):
40 self.upload = FakeMethod()
41
42
43class TestSnapBuildJob(TestCaseWithFactory):
44
45 layer = DatabaseFunctionalLayer
46
47 def setUp(self):
48 super(TestSnapBuildJob, self).setUp()
49 self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
50
51 def test_provides_interface(self):
52 # `SnapBuildJob` objects provide `ISnapBuildJob`.
53 snapbuild = self.factory.makeSnapBuild()
54 self.assertProvides(
55 SnapBuildJob(snapbuild, SnapBuildJobType.STORE_UPLOAD, {}),
56 ISnapBuildJob)
57
58
59class TestSnapStoreUploadJob(TestCaseWithFactory):
60
61 layer = LaunchpadZopelessLayer
62
63 def setUp(self):
64 super(TestSnapStoreUploadJob, self).setUp()
65 self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
66
67 def test_provides_interface(self):
68 # `SnapStoreUploadJob` objects provide `ISnapStoreUploadJob`.
69 snapbuild = self.factory.makeSnapBuild()
70 job = SnapStoreUploadJob.create(snapbuild)
71 self.assertProvides(job, ISnapStoreUploadJob)
72
73 def test___repr__(self):
74 # `SnapStoreUploadJob` objects have an informative __repr__.
75 snapbuild = self.factory.makeSnapBuild()
76 job = SnapStoreUploadJob.create(snapbuild)
77 self.assertEqual(
78 "<SnapStoreUploadJob for %s>" % snapbuild.title, repr(job))
79
80 def test_run(self):
81 # The job uploads the build to the store.
82 snapbuild = self.factory.makeSnapBuild()
83 self.assertContentEqual([], snapbuild.store_upload_jobs)
84 job = SnapStoreUploadJob.create(snapbuild)
85 client = FakeSnapStoreClient()
86 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
87 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
88 JobRunner([job]).runAll()
89 self.assertEqual([((snapbuild,), {})], client.upload.calls)
90 self.assertContentEqual([job], snapbuild.store_upload_jobs)
91 self.assertIsNone(job.error_message)
92
93 def test_run_failed(self):
94 # A failed run sets the store upload status to FAILED.
95 snapbuild = self.factory.makeSnapBuild()
96 self.assertContentEqual([], snapbuild.store_upload_jobs)
97 job = SnapStoreUploadJob.create(snapbuild)
98 client = FakeSnapStoreClient()
99 client.upload.failure = ValueError("An upload failure")
100 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
101 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
102 JobRunner([job]).runAll()
103 self.assertEqual([((snapbuild,), {})], client.upload.calls)
104 self.assertContentEqual([job], snapbuild.store_upload_jobs)
105 self.assertEqual("An upload failure", job.error_message)