Merge lp:~cjwatson/launchpad/snap-initial-name-bzr into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18715
Proposed branch: lp:~cjwatson/launchpad/snap-initial-name-bzr
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/better-bzr-mp-diffs
Diff against target: 1044 lines (+564/-106)
9 files modified
lib/lp/code/interfaces/branch.py (+9/-0)
lib/lp/code/model/branch.py (+63/-0)
lib/lp/code/model/tests/test_branch.py (+152/-1)
lib/lp/code/tests/helpers.py (+21/-2)
lib/lp/snappy/browser/snap.py (+13/-30)
lib/lp/snappy/browser/tests/test_snap.py (+69/-72)
lib/lp/snappy/interfaces/snap.py (+26/-0)
lib/lp/snappy/model/snap.py (+59/-1)
lib/lp/snappy/tests/test_snap.py (+152/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-initial-name-bzr
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+345757@code.launchpad.net

Commit message

Extract initial Snap.store_name from snapcraft.yaml for Bazaar as well as Git.

Description of the change

The potential timeout issues will be at least as bad as with git, but BranchHostingClient has all the same sort of timeout management code as we put together for GitHostingClient, which should mitigate that.

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 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py 2018-06-14 17:09:31 +0000
+++ lib/lp/code/interfaces/branch.py 2018-06-14 17:09:31 +0000
@@ -765,6 +765,15 @@
765 :param launchbag: `ILaunchBag`.765 :param launchbag: `ILaunchBag`.
766 """766 """
767767
768 def getBlob(filename, revision_id=None):
769 """Get a blob by file name from this branch.
770
771 :param filename: Relative path of a file in the branch.
772 :param revision_id: An optional revision ID. Defaults to the last
773 scanned revision ID of the branch.
774 :return: The blob content as a byte string.
775 """
776
768 def getDiff(new, old):777 def getDiff(new, old):
769 """Get the diff between two revisions in this branch.778 """Get the diff between two revisions in this branch.
770779
771780
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2018-06-14 17:09:31 +0000
+++ lib/lp/code/model/branch.py 2018-06-14 17:09:31 +0000
@@ -10,12 +10,15 @@
1010
11from datetime import datetime11from datetime import datetime
12from functools import partial12from functools import partial
13import json
13import operator14import operator
15import os.path
1416
15from bzrlib import urlutils17from bzrlib import urlutils
16from bzrlib.revision import NULL_REVISION18from bzrlib.revision import NULL_REVISION
17from lazr.lifecycle.event import ObjectCreatedEvent19from lazr.lifecycle.event import ObjectCreatedEvent
18import pytz20import pytz
21from six.moves.urllib_parse import urlsplit
19from sqlobject import (22from sqlobject import (
20 ForeignKey,23 ForeignKey,
21 IntCol,24 IntCol,
@@ -84,6 +87,7 @@
84 )87 )
85from lp.code.errors import (88from lp.code.errors import (
86 AlreadyLatestFormat,89 AlreadyLatestFormat,
90 BranchFileNotFound,
87 BranchMergeProposalExists,91 BranchMergeProposalExists,
88 BranchTargetError,92 BranchTargetError,
89 BranchTypeError,93 BranchTypeError,
@@ -172,10 +176,12 @@
172 ArrayAgg,176 ArrayAgg,
173 ArrayIntersects,177 ArrayIntersects,
174 )178 )
179from lp.services.features import getFeatureFlag
175from lp.services.helpers import shortlist180from lp.services.helpers import shortlist
176from lp.services.job.interfaces.job import JobStatus181from lp.services.job.interfaces.job import JobStatus
177from lp.services.job.model.job import Job182from lp.services.job.model.job import Job
178from lp.services.mail.notificationrecipientset import NotificationRecipientSet183from lp.services.mail.notificationrecipientset import NotificationRecipientSet
184from lp.services.memcache.interfaces import IMemcacheClient
179from lp.services.propertycache import (185from lp.services.propertycache import (
180 cachedproperty,186 cachedproperty,
181 get_property_cache,187 get_property_cache,
@@ -783,6 +789,63 @@
783 RevisionAuthor, revisions, ['revision_author_id'])789 RevisionAuthor, revisions, ['revision_author_id'])
784 return DecoratedResultSet(result, pre_iter_hook=eager_load)790 return DecoratedResultSet(result, pre_iter_hook=eager_load)
785791
792 def getBlob(self, filename, revision_id=None, enable_memcache=None,
793 logger=None):
794 """See `IBranch`."""
795 hosting_client = getUtility(IBranchHostingClient)
796 if enable_memcache is None:
797 enable_memcache = not getFeatureFlag(
798 u'code.bzr.blob.disable_memcache')
799 if revision_id is None:
800 revision_id = self.last_scanned_id
801 if revision_id is None:
802 # revision_id may still be None if the branch scanner hasn't
803 # scanned this branch yet. In this case, we won't be able to
804 # guarantee that subsequent calls to this method within the same
805 # transaction with revision_id=None will see the same revision,
806 # and we won't be able to cache file lists. Neither is fatal,
807 # and this should be relatively rare.
808 enable_memcache = False
809 dirname = os.path.dirname(filename)
810 unset = object()
811 file_list = unset
812 if enable_memcache:
813 memcache_client = getUtility(IMemcacheClient)
814 instance_name = urlsplit(
815 config.codehosting.internal_bzr_api_endpoint).hostname
816 memcache_key = '%s:bzr-file-list:%s:%s:%s' % (
817 instance_name, self.id, revision_id, dirname)
818 if not isinstance(memcache_key, bytes):
819 memcache_key = memcache_key.encode('UTF-8')
820 cached_file_list = memcache_client.get(memcache_key)
821 if cached_file_list is not None:
822 try:
823 file_list = json.loads(cached_file_list)
824 except Exception:
825 logger.exception(
826 'Cannot load cached file list for %s:%s:%s; deleting' %
827 (self.unique_name, revision_id, dirname))
828 memcache_client.delete(memcache_key)
829 if file_list is unset:
830 try:
831 inventory = hosting_client.getInventory(
832 self.unique_name, dirname, rev=revision_id)
833 file_list = {
834 entry['filename']: entry['file_id']
835 for entry in inventory['filelist']}
836 except BranchFileNotFound:
837 file_list = None
838 if enable_memcache:
839 # Cache the file list in case there's a request for another
840 # file in the same directory.
841 memcache_client.set(memcache_key, json.dumps(file_list))
842 file_id = (file_list or {}).get(os.path.basename(filename))
843 if file_id is None:
844 raise BranchFileNotFound(
845 self.unique_name, filename=filename, rev=revision_id)
846 return hosting_client.getBlob(
847 self.unique_name, file_id, rev=revision_id)
848
786 def getDiff(self, new, old=None):849 def getDiff(self, new, old=None):
787 """See `IBranch`."""850 """See `IBranch`."""
788 hosting_client = getUtility(IBranchHostingClient)851 hosting_client = getUtility(IBranchHostingClient)
789852
=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py 2018-03-06 00:59:06 +0000
+++ lib/lp/code/model/tests/test_branch.py 2018-06-14 17:09:31 +0000
@@ -11,6 +11,7 @@
11 datetime,11 datetime,
12 timedelta,12 timedelta,
13 )13 )
14import json
1415
15from bzrlib.branch import Branch16from bzrlib.branch import Branch
16from bzrlib.bzrdir import BzrDir17from bzrlib.bzrdir import BzrDir
@@ -61,6 +62,7 @@
61 AlreadyLatestFormat,62 AlreadyLatestFormat,
62 BranchCreatorNotMemberOfOwnerTeam,63 BranchCreatorNotMemberOfOwnerTeam,
63 BranchCreatorNotOwner,64 BranchCreatorNotOwner,
65 BranchFileNotFound,
64 BranchTargetError,66 BranchTargetError,
65 CannotDeleteBranch,67 CannotDeleteBranch,
66 CannotUpgradeNonHosted,68 CannotUpgradeNonHosted,
@@ -109,7 +111,10 @@
109from lp.code.model.branchrevision import BranchRevision111from lp.code.model.branchrevision import BranchRevision
110from lp.code.model.codereviewcomment import CodeReviewComment112from lp.code.model.codereviewcomment import CodeReviewComment
111from lp.code.model.revision import Revision113from lp.code.model.revision import Revision
112from lp.code.tests.helpers import add_revision_to_branch114from lp.code.tests.helpers import (
115 add_revision_to_branch,
116 BranchHostingFixture,
117 )
113from lp.codehosting.safe_open import BadUrl118from lp.codehosting.safe_open import BadUrl
114from lp.codehosting.vfs.branchfs import get_real_branch_path119from lp.codehosting.vfs.branchfs import get_real_branch_path
115from lp.registry.enums import (120from lp.registry.enums import (
@@ -136,6 +141,7 @@
136 block_on_job,141 block_on_job,
137 monitor_celery,142 monitor_celery,
138 )143 )
144from lp.services.memcache.interfaces import IMemcacheClient
139from lp.services.osutils import override_environ145from lp.services.osutils import override_environ
140from lp.services.propertycache import clear_property_cache146from lp.services.propertycache import clear_property_cache
141from lp.services.webapp.authorization import check_permission147from lp.services.webapp.authorization import check_permission
@@ -159,6 +165,7 @@
159 )165 )
160from lp.testing.dbuser import dbuser166from lp.testing.dbuser import dbuser
161from lp.testing.factory import LaunchpadObjectFactory167from lp.testing.factory import LaunchpadObjectFactory
168from lp.testing.fakemethod import FakeMethod
162from lp.testing.layers import (169from lp.testing.layers import (
163 CeleryBranchWriteJobLayer,170 CeleryBranchWriteJobLayer,
164 CeleryBzrsyncdJobLayer,171 CeleryBzrsyncdJobLayer,
@@ -3293,6 +3300,150 @@
3293 self.assertRaises(BadUrl, db_stacked.getBzrBranch)3300 self.assertRaises(BadUrl, db_stacked.getBzrBranch)
32943301
32953302
3303class TestBranchGetBlob(TestCaseWithFactory):
3304
3305 layer = DatabaseFunctionalLayer
3306
3307 def test_default_rev_unscanned(self):
3308 branch = self.factory.makeBranch()
3309 hosting_fixture = self.useFixture(BranchHostingFixture(
3310 file_list={'README.txt': 'some-file-id'}, blob=b'Some text'))
3311 blob = branch.getBlob('src/README.txt')
3312 self.assertEqual('Some text', blob)
3313 self.assertEqual(
3314 [((branch.unique_name, 'src'), {'rev': None})],
3315 hosting_fixture.getInventory.calls)
3316 self.assertEqual(
3317 [((branch.unique_name, 'some-file-id'), {'rev': None})],
3318 hosting_fixture.getBlob.calls)
3319 self.assertEqual({}, getUtility(IMemcacheClient)._cache)
3320
3321 def test_default_rev_scanned(self):
3322 branch = self.factory.makeBranch()
3323 removeSecurityProxy(branch).last_scanned_id = 'scanned-id'
3324 hosting_fixture = self.useFixture(BranchHostingFixture(
3325 file_list={'README.txt': 'some-file-id'}, blob=b'Some text'))
3326 blob = branch.getBlob('src/README.txt')
3327 self.assertEqual('Some text', blob)
3328 self.assertEqual(
3329 [((branch.unique_name, 'src'), {'rev': 'scanned-id'})],
3330 hosting_fixture.getInventory.calls)
3331 self.assertEqual(
3332 [((branch.unique_name, 'some-file-id'), {'rev': 'scanned-id'})],
3333 hosting_fixture.getBlob.calls)
3334 key = (
3335 'bazaar.launchpad.dev:bzr-file-list:%s:scanned-id:src' % branch.id)
3336 self.assertEqual(
3337 json.dumps({'README.txt': 'some-file-id'}),
3338 getUtility(IMemcacheClient).get(key.encode('UTF-8')))
3339
3340 def test_with_rev(self):
3341 branch = self.factory.makeBranch()
3342 hosting_fixture = self.useFixture(BranchHostingFixture(
3343 file_list={'README.txt': 'some-file-id'}, blob=b'Some text'))
3344 blob = branch.getBlob('src/README.txt', revision_id='some-rev')
3345 self.assertEqual('Some text', blob)
3346 self.assertEqual(
3347 [((branch.unique_name, 'src'), {'rev': 'some-rev'})],
3348 hosting_fixture.getInventory.calls)
3349 self.assertEqual(
3350 [((branch.unique_name, 'some-file-id'), {'rev': 'some-rev'})],
3351 hosting_fixture.getBlob.calls)
3352 key = 'bazaar.launchpad.dev:bzr-file-list:%s:some-rev:src' % branch.id
3353 self.assertEqual(
3354 json.dumps({'README.txt': 'some-file-id'}),
3355 getUtility(IMemcacheClient).get(key.encode('UTF-8')))
3356
3357 def test_cached_inventory(self):
3358 branch = self.factory.makeBranch()
3359 hosting_fixture = self.useFixture(BranchHostingFixture(
3360 blob=b'Some text'))
3361 key = 'bazaar.launchpad.dev:bzr-file-list:%s:some-rev:src' % branch.id
3362 getUtility(IMemcacheClient).set(
3363 key.encode('UTF-8'), json.dumps({'README.txt': 'some-file-id'}))
3364 blob = branch.getBlob('src/README.txt', revision_id='some-rev')
3365 self.assertEqual('Some text', blob)
3366 self.assertEqual([], hosting_fixture.getInventory.calls)
3367 self.assertEqual(
3368 [((branch.unique_name, 'some-file-id'), {'rev': 'some-rev'})],
3369 hosting_fixture.getBlob.calls)
3370
3371 def test_disable_memcache(self):
3372 self.useFixture(FeatureFixture(
3373 {'code.bzr.blob.disable_memcache': 'on'}))
3374 branch = self.factory.makeBranch()
3375 hosting_fixture = self.useFixture(BranchHostingFixture(
3376 file_list={'README.txt': 'some-file-id'}, blob=b'Some text'))
3377 key = 'bazaar.launchpad.dev:bzr-file-list:%s:some-rev:src' % branch.id
3378 getUtility(IMemcacheClient).set(key.encode('UTF-8'), '{}')
3379 blob = branch.getBlob('src/README.txt', revision_id='some-rev')
3380 self.assertEqual('Some text', blob)
3381 self.assertEqual(
3382 [((branch.unique_name, 'src'), {'rev': 'some-rev'})],
3383 hosting_fixture.getInventory.calls)
3384 self.assertEqual(
3385 '{}', getUtility(IMemcacheClient).get(key.encode('UTF-8')))
3386
3387 def test_file_at_root_of_branch(self):
3388 branch = self.factory.makeBranch()
3389 hosting_fixture = self.useFixture(BranchHostingFixture(
3390 file_list={'README.txt': 'some-file-id'}, blob=b'Some text'))
3391 blob = branch.getBlob('README.txt', revision_id='some-rev')
3392 self.assertEqual('Some text', blob)
3393 self.assertEqual(
3394 [((branch.unique_name, ''), {'rev': 'some-rev'})],
3395 hosting_fixture.getInventory.calls)
3396 self.assertEqual(
3397 [((branch.unique_name, 'some-file-id'), {'rev': 'some-rev'})],
3398 hosting_fixture.getBlob.calls)
3399 key = 'bazaar.launchpad.dev:bzr-file-list:%s:some-rev:' % branch.id
3400 self.assertEqual(
3401 json.dumps({'README.txt': 'some-file-id'}),
3402 getUtility(IMemcacheClient).get(key.encode('UTF-8')))
3403
3404 def test_file_not_in_directory(self):
3405 branch = self.factory.makeBranch()
3406 hosting_fixture = self.useFixture(BranchHostingFixture(file_list={}))
3407 self.assertRaises(
3408 BranchFileNotFound, branch.getBlob,
3409 'src/README.txt', revision_id='some-rev')
3410 self.assertEqual(
3411 [((branch.unique_name, 'src'), {'rev': 'some-rev'})],
3412 hosting_fixture.getInventory.calls)
3413 self.assertEqual([], hosting_fixture.getBlob.calls)
3414 key = 'bazaar.launchpad.dev:bzr-file-list:%s:some-rev:src' % branch.id
3415 self.assertEqual(
3416 '{}', getUtility(IMemcacheClient).get(key.encode('UTF-8')))
3417
3418 def test_missing_directory(self):
3419 branch = self.factory.makeBranch()
3420 hosting_fixture = self.useFixture(BranchHostingFixture())
3421 hosting_fixture.getInventory = FakeMethod(
3422 failure=BranchFileNotFound(
3423 branch.unique_name, filename='src', rev='some-rev'))
3424 self.assertRaises(
3425 BranchFileNotFound, branch.getBlob,
3426 'src/README.txt', revision_id='some-rev')
3427 self.assertEqual(
3428 [((branch.unique_name, 'src'), {'rev': 'some-rev'})],
3429 hosting_fixture.getInventory.calls)
3430 self.assertEqual([], hosting_fixture.getBlob.calls)
3431 key = 'bazaar.launchpad.dev:bzr-file-list:%s:some-rev:src' % branch.id
3432 self.assertEqual(
3433 'null', getUtility(IMemcacheClient).get(key.encode('UTF-8')))
3434
3435 def test_cached_missing_directory(self):
3436 branch = self.factory.makeBranch()
3437 hosting_fixture = self.useFixture(BranchHostingFixture())
3438 key = 'bazaar.launchpad.dev:bzr-file-list:%s:some-rev:src' % branch.id
3439 getUtility(IMemcacheClient).set(key.encode('UTF-8'), 'null')
3440 self.assertRaises(
3441 BranchFileNotFound, branch.getBlob,
3442 'src/README.txt', revision_id='some-rev')
3443 self.assertEqual([], hosting_fixture.getInventory.calls)
3444 self.assertEqual([], hosting_fixture.getBlob.calls)
3445
3446
3296class TestBranchUnscan(TestCaseWithFactory):3447class TestBranchUnscan(TestCaseWithFactory):
32973448
3298 layer = DatabaseFunctionalLayer3449 layer = DatabaseFunctionalLayer
32993450
=== modified file 'lib/lp/code/tests/helpers.py'
--- lib/lp/code/tests/helpers.py 2018-06-14 17:09:31 +0000
+++ lib/lp/code/tests/helpers.py 2018-06-14 17:09:31 +0000
@@ -306,14 +306,33 @@
306class BranchHostingFixture(fixtures.Fixture):306class BranchHostingFixture(fixtures.Fixture):
307 """A fixture that temporarily registers a fake Bazaar hosting client."""307 """A fixture that temporarily registers a fake Bazaar hosting client."""
308308
309 def __init__(self, diff=None, inventory=None, blob=None):309 def __init__(self, diff=None, inventory=None, file_list=None, blob=None,
310 disable_memcache=True):
310 self.create = FakeMethod()311 self.create = FakeMethod()
311 self.getDiff = FakeMethod(result=diff or {})312 self.getDiff = FakeMethod(result=diff or {})
312 self.getInventory = FakeMethod(result=inventory or {})313 if inventory is None:
314 if file_list is not None:
315 # Simple common case.
316 inventory = {
317 "filelist": [
318 {"filename": filename, "file_id": file_id}
319 for filename, file_id in file_list.items()],
320 }
321 else:
322 inventory = {"filelist": []}
323 self.getInventory = FakeMethod(result=inventory)
313 self.getBlob = FakeMethod(result=blob)324 self.getBlob = FakeMethod(result=blob)
325 self.disable_memcache = disable_memcache
314326
315 def _setUp(self):327 def _setUp(self):
316 self.useFixture(ZopeUtilityFixture(self, IBranchHostingClient))328 self.useFixture(ZopeUtilityFixture(self, IBranchHostingClient))
329 if self.disable_memcache:
330 # Most tests that involve Branch.getBlob don't want to cache the
331 # result: doing so requires more time-consuming test setup and
332 # makes it awkward to repeat the same call with different
333 # responses. For convenience, we make it easy to disable that
334 # here.
335 self.memcache_fixture = self.useFixture(MemcacheFixture())
317336
318337
319class GitHostingFixture(fixtures.Fixture):338class GitHostingFixture(fixtures.Fixture):
320339
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 2018-04-21 10:01:22 +0000
+++ lib/lp/snappy/browser/snap.py 2018-06-14 17:09:31 +0000
@@ -23,7 +23,6 @@
23 copy_field,23 copy_field,
24 use_template,24 use_template,
25 )25 )
26import yaml
27from zope.component import getUtility26from zope.component import getUtility
28from zope.error.interfaces import IErrorReportingUtility27from zope.error.interfaces import IErrorReportingUtility
29from zope.interface import Interface28from zope.interface import Interface
@@ -44,6 +43,7 @@
44from lp.app.browser.lazrjs import InlinePersonEditPickerWidget43from lp.app.browser.lazrjs import InlinePersonEditPickerWidget
45from lp.app.browser.tales import format_link44from lp.app.browser.tales import format_link
46from lp.app.enums import PRIVATE_INFORMATION_TYPES45from lp.app.enums import PRIVATE_INFORMATION_TYPES
46from lp.app.errors import NotFoundError
47from lp.app.interfaces.informationtype import IInformationType47from lp.app.interfaces.informationtype import IInformationType
48from lp.app.widgets.itemswidgets import (48from lp.app.widgets.itemswidgets import (
49 LabeledMultiCheckBoxWidget,49 LabeledMultiCheckBoxWidget,
@@ -52,10 +52,6 @@
52 )52 )
53from lp.buildmaster.interfaces.processor import IProcessorSet53from lp.buildmaster.interfaces.processor import IProcessorSet
54from lp.code.browser.widgets.gitref import GitRefWidget54from lp.code.browser.widgets.gitref import GitRefWidget
55from lp.code.errors import (
56 GitRepositoryBlobNotFound,
57 GitRepositoryScanFault,
58 )
59from lp.code.interfaces.gitref import IGitRef55from lp.code.interfaces.gitref import IGitRef
60from lp.registry.enums import VCSType56from lp.registry.enums import VCSType
61from lp.registry.interfaces.pocket import PackagePublishingPocket57from lp.registry.interfaces.pocket import PackagePublishingPocket
@@ -83,6 +79,8 @@
83from lp.snappy.browser.widgets.storechannels import StoreChannelsWidget79from lp.snappy.browser.widgets.storechannels import StoreChannelsWidget
84from lp.snappy.interfaces.snap import (80from lp.snappy.interfaces.snap import (
85 CannotAuthorizeStoreUploads,81 CannotAuthorizeStoreUploads,
82 CannotFetchSnapcraftYaml,
83 CannotParseSnapcraftYaml,
86 ISnap,84 ISnap,
87 ISnapSet,85 ISnapSet,
88 NoSuchSnap,86 NoSuchSnap,
@@ -424,34 +422,19 @@
424 @property422 @property
425 def initial_values(self):423 def initial_values(self):
426 store_name = None424 store_name = None
427 if self.has_snappy_distro_series and IGitRef.providedBy(self.context):425 if self.has_snappy_distro_series:
428 # Try to extract Snap store name from snapcraft.yaml file.426 # Try to extract Snap store name from snapcraft.yaml file.
429 try:427 try:
430 paths = (428 snapcraft_data = getUtility(ISnapSet).getSnapcraftYaml(
431 'snap/snapcraft.yaml',429 self.context, logger=log)
432 'snapcraft.yaml',430 except (NotFoundError, CannotFetchSnapcraftYaml,
433 '.snapcraft.yaml',431 CannotParseSnapcraftYaml):
434 )
435 for i, path in enumerate(paths):
436 try:
437 blob = self.context.repository.getBlob(
438 path, self.context.name)
439 break
440 except GitRepositoryBlobNotFound:
441 if i == len(paths) - 1:
442 raise
443 # Beware of unsafe yaml.load()!
444 store_name = yaml.safe_load(blob).get('name')
445 except GitRepositoryScanFault:
446 log.exception("Failed to get Snap manifest from Git %s",
447 self.context.unique_name)
448 except (AttributeError, yaml.YAMLError):
449 # Ignore parsing errors from invalid, user-supplied YAML
450 pass432 pass
451 except Exception as e:433 else:
452 log.exception(434 try:
453 "Failed to extract name from Snap manifest at Git %s: %s",435 store_name = snapcraft_data.get('name')
454 self.context.unique_name, unicode(e))436 except AttributeError:
437 pass
455438
456 store_series = getUtility(ISnappySeriesSet).getAll().first()439 store_series = getUtility(ISnappySeriesSet).getAll().first()
457 if store_series.preferred_distro_series is not None:440 if store_series.preferred_distro_series is not None:
458441
=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py 2018-05-31 10:23:03 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py 2018-06-14 17:09:31 +0000
@@ -21,7 +21,6 @@
2121
22from fixtures import FakeLogger22from fixtures import FakeLogger
23from mechanize import LinkNotFoundError23from mechanize import LinkNotFoundError
24import mock
25from pymacaroons import Macaroon24from pymacaroons import Macaroon
26import pytz25import pytz
27import responses26import responses
@@ -40,10 +39,13 @@
40from lp.buildmaster.enums import BuildStatus39from lp.buildmaster.enums import BuildStatus
41from lp.buildmaster.interfaces.processor import IProcessorSet40from lp.buildmaster.interfaces.processor import IProcessorSet
42from lp.code.errors import (41from lp.code.errors import (
43 GitRepositoryBlobNotFound,42 BranchHostingFault,
44 GitRepositoryScanFault,43 GitRepositoryScanFault,
45 )44 )
46from lp.code.tests.helpers import GitHostingFixture45from lp.code.tests.helpers import (
46 BranchHostingFixture,
47 GitHostingFixture,
48 )
47from lp.registry.enums import PersonVisibility49from lp.registry.enums import PersonVisibility
48from lp.registry.interfaces.pocket import PackagePublishingPocket50from lp.registry.interfaces.pocket import PackagePublishingPocket
49from lp.registry.interfaces.series import SeriesStatus51from lp.registry.interfaces.series import SeriesStatus
@@ -128,6 +130,7 @@
128 def test_private_feature_flag_disabled(self):130 def test_private_feature_flag_disabled(self):
129 # Without a private_snap feature flag, we will not create Snaps for131 # Without a private_snap feature flag, we will not create Snaps for
130 # private contexts.132 # private contexts.
133 self.useFixture(BranchHostingFixture())
131 self.snap_store_client = FakeMethod()134 self.snap_store_client = FakeMethod()
132 self.snap_store_client.listChannels = FakeMethod(result=[])135 self.snap_store_client.listChannels = FakeMethod(result=[])
133 self.useFixture(136 self.useFixture(
@@ -197,6 +200,7 @@
197 def test_initial_store_distro_series(self):200 def test_initial_store_distro_series(self):
198 # The initial store_distro_series uses the preferred distribution201 # The initial store_distro_series uses the preferred distribution
199 # series for the latest snappy series.202 # series for the latest snappy series.
203 self.useFixture(BranchHostingFixture(blob=b""))
200 lts = self.factory.makeUbuntuDistroSeries(204 lts = self.factory.makeUbuntuDistroSeries(
201 version="16.04", status=SeriesStatus.CURRENT)205 version="16.04", status=SeriesStatus.CURRENT)
202 current = self.factory.makeUbuntuDistroSeries(206 current = self.factory.makeUbuntuDistroSeries(
@@ -221,6 +225,7 @@
221 no_login=True)225 no_login=True)
222226
223 def test_create_new_snap_bzr(self):227 def test_create_new_snap_bzr(self):
228 self.useFixture(BranchHostingFixture(blob=b""))
224 branch = self.factory.makeAnyBranch()229 branch = self.factory.makeAnyBranch()
225 source_display = branch.display_name230 source_display = branch.display_name
226 browser = self.getViewBrowser(231 browser = self.getViewBrowser(
@@ -257,7 +262,7 @@
257 MatchesTagText(content, "store_upload"))262 MatchesTagText(content, "store_upload"))
258263
259 def test_create_new_snap_git(self):264 def test_create_new_snap_git(self):
260 self.useFixture(GitHostingFixture(blob=""))265 self.useFixture(GitHostingFixture(blob=b""))
261 [git_ref] = self.factory.makeGitRefs()266 [git_ref] = self.factory.makeGitRefs()
262 source_display = git_ref.display_name267 source_display = git_ref.display_name
263 browser = self.getViewBrowser(268 browser = self.getViewBrowser(
@@ -295,6 +300,7 @@
295300
296 def test_create_new_snap_users_teams_as_owner_options(self):301 def test_create_new_snap_users_teams_as_owner_options(self):
297 # Teams that the user is in are options for the snap package owner.302 # Teams that the user is in are options for the snap package owner.
303 self.useFixture(BranchHostingFixture(blob=b""))
298 self.factory.makeTeam(304 self.factory.makeTeam(
299 name="test-team", displayname="Test Team", members=[self.person])305 name="test-team", displayname="Test Team", members=[self.person])
300 branch = self.factory.makeAnyBranch()306 branch = self.factory.makeAnyBranch()
@@ -307,6 +313,7 @@
307313
308 def test_create_new_snap_public(self):314 def test_create_new_snap_public(self):
309 # Public owner implies public snap.315 # Public owner implies public snap.
316 self.useFixture(BranchHostingFixture(blob=b""))
310 branch = self.factory.makeAnyBranch()317 branch = self.factory.makeAnyBranch()
311318
312 browser = self.getViewBrowser(319 browser = self.getViewBrowser(
@@ -339,6 +346,7 @@
339346
340 def test_create_new_snap_private(self):347 def test_create_new_snap_private(self):
341 # Private teams will automatically create private snaps.348 # Private teams will automatically create private snaps.
349 self.useFixture(BranchHostingFixture(blob=b""))
342 login_person(self.person)350 login_person(self.person)
343 self.factory.makeTeam(351 self.factory.makeTeam(
344 name='super-private', owner=self.person,352 name='super-private', owner=self.person,
@@ -360,6 +368,7 @@
360368
361 def test_create_new_snap_build_source_tarball(self):369 def test_create_new_snap_build_source_tarball(self):
362 # We can create a new snap and ask for it to build a source tarball.370 # We can create a new snap and ask for it to build a source tarball.
371 self.useFixture(BranchHostingFixture(blob=b""))
363 branch = self.factory.makeAnyBranch()372 branch = self.factory.makeAnyBranch()
364 browser = self.getViewBrowser(373 browser = self.getViewBrowser(
365 branch, view_name="+new-snap", user=self.person)374 branch, view_name="+new-snap", user=self.person)
@@ -375,6 +384,7 @@
375 def test_create_new_snap_auto_build(self):384 def test_create_new_snap_auto_build(self):
376 # Creating a new snap and asking for it to be automatically built385 # Creating a new snap and asking for it to be automatically built
377 # sets all the appropriate fields.386 # sets all the appropriate fields.
387 self.useFixture(BranchHostingFixture(blob=b""))
378 branch = self.factory.makeAnyBranch()388 branch = self.factory.makeAnyBranch()
379 archive = self.factory.makeArchive()389 archive = self.factory.makeArchive()
380 browser = self.getViewBrowser(390 browser = self.getViewBrowser(
@@ -405,6 +415,7 @@
405 # Creating a new snap and asking for it to be automatically uploaded415 # Creating a new snap and asking for it to be automatically uploaded
406 # to the store sets all the appropriate fields and redirects to SSO416 # to the store sets all the appropriate fields and redirects to SSO
407 # for authorization.417 # for authorization.
418 self.useFixture(BranchHostingFixture(blob=b""))
408 branch = self.factory.makeAnyBranch()419 branch = self.factory.makeAnyBranch()
409 view_url = canonical_url(branch, view_name="+new-snap")420 view_url = canonical_url(branch, view_name="+new-snap")
410 browser = self.getNonRedirectingBrowser(url=view_url, user=self.person)421 browser = self.getNonRedirectingBrowser(url=view_url, user=self.person)
@@ -460,6 +471,7 @@
460 self.assertEqual(expected_args, parse_qs(parsed_location[3]))471 self.assertEqual(expected_args, parse_qs(parsed_location[3]))
461472
462 def test_create_new_snap_display_processors(self):473 def test_create_new_snap_display_processors(self):
474 self.useFixture(BranchHostingFixture(blob=b""))
463 branch = self.factory.makeAnyBranch()475 branch = self.factory.makeAnyBranch()
464 self.setUpDistroSeries()476 self.setUpDistroSeries()
465 browser = self.getViewBrowser(477 browser = self.getViewBrowser(
@@ -473,6 +485,7 @@
473485
474 def test_create_new_snap_display_restricted_processors(self):486 def test_create_new_snap_display_restricted_processors(self):
475 # A restricted processor is shown disabled in the UI.487 # A restricted processor is shown disabled in the UI.
488 self.useFixture(BranchHostingFixture(blob=b""))
476 branch = self.factory.makeAnyBranch()489 branch = self.factory.makeAnyBranch()
477 distroseries = self.setUpDistroSeries()490 distroseries = self.setUpDistroSeries()
478 proc_armhf = self.factory.makeProcessor(491 proc_armhf = self.factory.makeProcessor(
@@ -487,6 +500,7 @@
487 processors, ["386", "amd64", "hppa"], ["armhf"])500 processors, ["386", "amd64", "hppa"], ["armhf"])
488501
489 def test_create_new_snap_processors(self):502 def test_create_new_snap_processors(self):
503 self.useFixture(BranchHostingFixture(blob=b""))
490 branch = self.factory.makeAnyBranch()504 branch = self.factory.makeAnyBranch()
491 self.setUpDistroSeries()505 self.setUpDistroSeries()
492 browser = self.getViewBrowser(506 browser = self.getViewBrowser(
@@ -500,74 +514,57 @@
500 self.assertContentEqual(514 self.assertContentEqual(
501 ["386", "amd64"], [proc.name for proc in snap.processors])515 ["386", "amd64"], [proc.name for proc in snap.processors])
502516
503 def test_initial_name_extraction_git_snap_snapcraft_yaml(self):517 def test_initial_name_extraction_bzr_success(self):
504 def getBlob(filename, *args, **kwargs):518 self.useFixture(BranchHostingFixture(
505 if filename == "snap/snapcraft.yaml":519 file_list={"snapcraft.yaml": "file-id"}, blob=b"name: test-snap"))
506 return "name: test-snap"520 branch = self.factory.makeBranch()
507 else:521 view = create_initialized_view(branch, "+new-snap")
508 raise GitRepositoryBlobNotFound("dummy", filename)522 initial_values = view.initial_values
509523 self.assertIn('store_name', initial_values)
510 [git_ref] = self.factory.makeGitRefs()524 self.assertEqual('test-snap', initial_values['store_name'])
511 git_ref.repository.getBlob = getBlob525
512 view = create_initialized_view(git_ref, "+new-snap")526 def test_initial_name_extraction_bzr_error(self):
513 initial_values = view.initial_values527 self.useFixture(BranchHostingFixture()).getInventory = FakeMethod(
514 self.assertIn('store_name', initial_values)528 failure=BranchHostingFault)
515 self.assertEqual('test-snap', initial_values['store_name'])529 branch = self.factory.makeBranch()
516530 view = create_initialized_view(branch, "+new-snap")
517 def test_initial_name_extraction_git_plain_snapcraft_yaml(self):531 initial_values = view.initial_values
518 def getBlob(filename, *args, **kwargs):532 self.assertIn('store_name', initial_values)
519 if filename == "snapcraft.yaml":533 self.assertIsNone(initial_values['store_name'])
520 return "name: test-snap"534
521 else:535 def test_initial_name_extraction_bzr_no_name(self):
522 raise GitRepositoryBlobNotFound("dummy", filename)536 self.useFixture(BranchHostingFixture(
523537 file_list={"snapcraft.yaml": "file-id"}, blob=b"some: nonsense"))
524 [git_ref] = self.factory.makeGitRefs()538 branch = self.factory.makeBranch()
525 git_ref.repository.getBlob = getBlob539 view = create_initialized_view(branch, "+new-snap")
526 view = create_initialized_view(git_ref, "+new-snap")540 initial_values = view.initial_values
527 initial_values = view.initial_values541 self.assertIn('store_name', initial_values)
528 self.assertIn('store_name', initial_values)542 self.assertIsNone(initial_values['store_name'])
529 self.assertEqual('test-snap', initial_values['store_name'])543
530544 def test_initial_name_extraction_git_success(self):
531 def test_initial_name_extraction_git_dot_snapcraft_yaml(self):545 self.useFixture(GitHostingFixture(blob=b"name: test-snap"))
532 def getBlob(filename, *args, **kwargs):546 [git_ref] = self.factory.makeGitRefs()
533 if filename == ".snapcraft.yaml":547 view = create_initialized_view(git_ref, "+new-snap")
534 return "name: test-snap"548 initial_values = view.initial_values
535 else:549 self.assertIn('store_name', initial_values)
536 raise GitRepositoryBlobNotFound("dummy", filename)550 self.assertEqual('test-snap', initial_values['store_name'])
537551
538 [git_ref] = self.factory.makeGitRefs()552 def test_initial_name_extraction_git_error(self):
539 git_ref.repository.getBlob = getBlob553 self.useFixture(GitHostingFixture()).getBlob = FakeMethod(
540 view = create_initialized_view(git_ref, "+new-snap")554 failure=GitRepositoryScanFault)
541 initial_values = view.initial_values555 [git_ref] = self.factory.makeGitRefs()
542 self.assertIn('store_name', initial_values)556 view = create_initialized_view(git_ref, "+new-snap")
543 self.assertEqual('test-snap', initial_values['store_name'])557 initial_values = view.initial_values
544558 self.assertIn('store_name', initial_values)
545 def test_initial_name_extraction_git_repo_error(self):559 self.assertIsNone(initial_values['store_name'])
546 [git_ref] = self.factory.makeGitRefs()560
547 git_ref.repository.getBlob = FakeMethod(failure=GitRepositoryScanFault)561 def test_initial_name_extraction_git_no_name(self):
548 view = create_initialized_view(git_ref, "+new-snap")562 self.useFixture(GitHostingFixture(blob=b"some: nonsense"))
549 initial_values = view.initial_values563 [git_ref] = self.factory.makeGitRefs()
550 self.assertIn('store_name', initial_values)564 view = create_initialized_view(git_ref, "+new-snap")
551 self.assertIsNone(initial_values['store_name'])565 initial_values = view.initial_values
552566 self.assertIn('store_name', initial_values)
553 def test_initial_name_extraction_git_invalid_data(self):567 self.assertIsNone(initial_values['store_name'])
554 for invalid_result in (None, 123, '', '[][]', '#name:test', ']'):
555 [git_ref] = self.factory.makeGitRefs()
556 git_ref.repository.getBlob = FakeMethod(result=invalid_result)
557 view = create_initialized_view(git_ref, "+new-snap")
558 initial_values = view.initial_values
559 self.assertIn('store_name', initial_values)
560 self.assertIsNone(initial_values['store_name'])
561
562 def test_initial_name_extraction_git_safe_yaml(self):
563 [git_ref] = self.factory.makeGitRefs()
564 git_ref.repository.getBlob = FakeMethod(result='Malicious YAML!')
565 view = create_initialized_view(git_ref, "+new-snap")
566 with mock.patch('yaml.load') as unsafe_load:
567 with mock.patch('yaml.safe_load') as safe_load:
568 view.initial_values
569 self.assertEqual(0, unsafe_load.call_count)
570 self.assertEqual(1, safe_load.call_count)
571568
572569
573class TestSnapAdminView(BaseTestSnapView):570class TestSnapAdminView(BaseTestSnapView):
574571
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py 2018-05-07 05:25:27 +0000
+++ lib/lp/snappy/interfaces/snap.py 2018-06-14 17:09:31 +0000
@@ -9,7 +9,9 @@
9 'BadSnapSearchContext',9 'BadSnapSearchContext',
10 'BadSnapSource',10 'BadSnapSource',
11 'CannotAuthorizeStoreUploads',11 'CannotAuthorizeStoreUploads',
12 'CannotFetchSnapcraftYaml',
12 'CannotModifySnapProcessor',13 'CannotModifySnapProcessor',
14 'CannotParseSnapcraftYaml',
13 'CannotRequestAutoBuilds',15 'CannotRequestAutoBuilds',
14 'DuplicateSnapName',16 'DuplicateSnapName',
15 'ISnap',17 'ISnap',
@@ -237,6 +239,14 @@
237 "because %s is not set." % field)239 "because %s is not set." % field)
238240
239241
242class CannotFetchSnapcraftYaml(Exception):
243 """Launchpad cannot fetch this snap package's snapcraft.yaml."""
244
245
246class CannotParseSnapcraftYaml(Exception):
247 """Launchpad cannot parse this snap package's snapcraft.yaml."""
248
249
240class ISnapView(Interface):250class ISnapView(Interface):
241 """`ISnap` attributes that require launchpad.View permission."""251 """`ISnap` attributes that require launchpad.View permission."""
242252
@@ -787,6 +797,22 @@
787 def preloadDataForSnaps(snaps, user):797 def preloadDataForSnaps(snaps, user):
788 """Load the data related to a list of snap packages."""798 """Load the data related to a list of snap packages."""
789799
800 def getSnapcraftYaml(context, logger=None):
801 """Fetch a package's snapcraft.yaml from code hosting, if possible.
802
803 :param context: Either an `ISnap` or the source branch for a snap
804 package.
805 :param logger: An optional logger.
806
807 :return: The package's parsed snapcraft.yaml.
808 :raises NotFoundError: if this package has no snapcraft.yaml.
809 :raises CannotFetchSnapcraftYaml: if it was not possible to fetch
810 snapcraft.yaml from the code hosting backend for some other
811 reason.
812 :raises CannotParseSnapcraftYaml: if the fetched snapcraft.yaml
813 cannot be parsed.
814 """
815
790 def makeAutoBuilds(logger=None):816 def makeAutoBuilds(logger=None):
791 """Create and return automatic builds for stale snap packages.817 """Create and return automatic builds for stale snap packages.
792818
793819
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2018-04-21 10:01:22 +0000
+++ lib/lp/snappy/model/snap.py 2018-06-14 17:09:31 +0000
@@ -32,6 +32,7 @@
32 Storm,32 Storm,
33 Unicode,33 Unicode,
34 )34 )
35import yaml
35from zope.component import (36from zope.component import (
36 getAdapter,37 getAdapter,
37 getUtility,38 getUtility,
@@ -42,7 +43,10 @@
4243
43from lp.app.browser.tales import DateTimeFormatterAPI44from lp.app.browser.tales import DateTimeFormatterAPI
44from lp.app.enums import PRIVATE_INFORMATION_TYPES45from lp.app.enums import PRIVATE_INFORMATION_TYPES
45from lp.app.errors import IncompatibleArguments46from lp.app.errors import (
47 IncompatibleArguments,
48 NotFoundError,
49 )
46from lp.app.interfaces.security import IAuthorization50from lp.app.interfaces.security import IAuthorization
47from lp.buildmaster.enums import BuildStatus51from lp.buildmaster.enums import BuildStatus
48from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet52from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
@@ -50,6 +54,12 @@
50from lp.buildmaster.model.buildfarmjob import BuildFarmJob54from lp.buildmaster.model.buildfarmjob import BuildFarmJob
51from lp.buildmaster.model.buildqueue import BuildQueue55from lp.buildmaster.model.buildqueue import BuildQueue
52from lp.buildmaster.model.processor import Processor56from lp.buildmaster.model.processor import Processor
57from lp.code.errors import (
58 BranchFileNotFound,
59 BranchHostingFault,
60 GitRepositoryBlobNotFound,
61 GitRepositoryScanFault,
62 )
53from lp.code.interfaces.branch import IBranch63from lp.code.interfaces.branch import IBranch
54from lp.code.interfaces.branchcollection import (64from lp.code.interfaces.branchcollection import (
55 IAllBranches,65 IAllBranches,
@@ -110,7 +120,9 @@
110 BadSnapSearchContext,120 BadSnapSearchContext,
111 BadSnapSource,121 BadSnapSource,
112 CannotAuthorizeStoreUploads,122 CannotAuthorizeStoreUploads,
123 CannotFetchSnapcraftYaml,
113 CannotModifySnapProcessor,124 CannotModifySnapProcessor,
125 CannotParseSnapcraftYaml,
114 CannotRequestAutoBuilds,126 CannotRequestAutoBuilds,
115 DuplicateSnapName,127 DuplicateSnapName,
116 ISnap,128 ISnap,
@@ -923,6 +935,52 @@
923 list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(935 list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
924 person_ids, need_validity=True))936 person_ids, need_validity=True))
925937
938 def getSnapcraftYaml(self, context, logger=None):
939 """See `ISnapSet`."""
940 if ISnap.providedBy(context):
941 context = context.source
942 try:
943 paths = (
944 "snap/snapcraft.yaml",
945 "snapcraft.yaml",
946 ".snapcraft.yaml",
947 )
948 for path in paths:
949 try:
950 if IBranch.providedBy(context):
951 blob = context.getBlob(path)
952 else:
953 blob = context.repository.getBlob(path, context.name)
954 break
955 except (BranchFileNotFound, GitRepositoryBlobNotFound):
956 pass
957 else:
958 msg = "Cannot find snapcraft.yaml in %s"
959 if logger is not None:
960 logger.exception(msg, context.unique_name)
961 raise NotFoundError(msg % context.unique_name)
962 except (BranchHostingFault, GitRepositoryScanFault) as e:
963 msg = "Failed to get snap manifest from %s"
964 if logger is not None:
965 logger.exception(msg, context.unique_name)
966 raise CannotFetchSnapcraftYaml(
967 "%s: %s" % (msg % context.unique_name, e))
968
969 try:
970 snapcraft_data = yaml.safe_load(blob)
971 except Exception as e:
972 # Don't bother logging parsing errors from user-supplied YAML.
973 raise CannotParseSnapcraftYaml(
974 "Cannot parse snapcraft.yaml from %s: %s" %
975 (context.unique_name, e))
976
977 if not isinstance(snapcraft_data, dict):
978 raise CannotParseSnapcraftYaml(
979 "The top level of snapcraft.yaml from %s is not a mapping" %
980 context.unique_name)
981
982 return snapcraft_data
983
926 @staticmethod984 @staticmethod
927 def _findStaleSnaps():985 def _findStaleSnaps():
928 """See `ISnapSet`."""986 """See `ISnapSet`."""
929987
=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py 2018-05-31 10:23:03 +0000
+++ lib/lp/snappy/tests/test_snap.py 2018-06-14 17:09:31 +0000
@@ -14,6 +14,7 @@
14import json14import json
15from urlparse import urlsplit15from urlparse import urlsplit
1616
17from fixtures import MockPatch
17from lazr.lifecycle.event import ObjectModifiedEvent18from lazr.lifecycle.event import ObjectModifiedEvent
18from pymacaroons import Macaroon19from pymacaroons import Macaroon
19import pytz20import pytz
@@ -43,6 +44,16 @@
43from lp.buildmaster.interfaces.processor import IProcessorSet44from lp.buildmaster.interfaces.processor import IProcessorSet
44from lp.buildmaster.model.buildfarmjob import BuildFarmJob45from lp.buildmaster.model.buildfarmjob import BuildFarmJob
45from lp.buildmaster.model.buildqueue import BuildQueue46from lp.buildmaster.model.buildqueue import BuildQueue
47from lp.code.errors import (
48 BranchFileNotFound,
49 BranchHostingFault,
50 GitRepositoryBlobNotFound,
51 GitRepositoryScanFault,
52 )
53from lp.code.tests.helpers import (
54 BranchHostingFixture,
55 GitHostingFixture,
56 )
46from lp.registry.enums import PersonVisibility57from lp.registry.enums import PersonVisibility
47from lp.registry.interfaces.distribution import IDistributionSet58from lp.registry.interfaces.distribution import IDistributionSet
48from lp.registry.interfaces.pocket import PackagePublishingPocket59from lp.registry.interfaces.pocket import PackagePublishingPocket
@@ -62,7 +73,9 @@
62from lp.services.webapp.interfaces import OAuthPermission73from lp.services.webapp.interfaces import OAuthPermission
63from lp.snappy.interfaces.snap import (74from lp.snappy.interfaces.snap import (
64 BadSnapSearchContext,75 BadSnapSearchContext,
76 CannotFetchSnapcraftYaml,
65 CannotModifySnapProcessor,77 CannotModifySnapProcessor,
78 CannotParseSnapcraftYaml,
66 ISnap,79 ISnap,
67 ISnapSet,80 ISnapSet,
68 ISnapView,81 ISnapView,
@@ -975,6 +988,145 @@
975 [snaps[0], snaps[2], snaps[4], snaps[6]],988 [snaps[0], snaps[2], snaps[4], snaps[6]],
976 getUtility(ISnapSet).findByURLPrefixes(prefixes, owner=owners[0]))989 getUtility(ISnapSet).findByURLPrefixes(prefixes, owner=owners[0]))
977990
991 def test_getSnapcraftYaml_bzr_snap_snapcraft_yaml(self):
992 def getInventory(unique_name, dirname, *args, **kwargs):
993 if dirname == "snap":
994 return {"filelist": [{
995 "filename": "snapcraft.yaml", "file_id": "some-file-id",
996 }]}
997 else:
998 raise BranchFileNotFound("dummy", dirname)
999
1000 self.useFixture(BranchHostingFixture(
1001 blob=b"name: test-snap")).getInventory = getInventory
1002 branch = self.factory.makeBranch()
1003 self.assertEqual(
1004 {"name": "test-snap"},
1005 getUtility(ISnapSet).getSnapcraftYaml(branch))
1006
1007 def test_getSnapcraftYaml_bzr_plain_snapcraft_yaml(self):
1008 def getInventory(unique_name, dirname, *args, **kwargs):
1009 if dirname == "":
1010 return {"filelist": [{
1011 "filename": "snapcraft.yaml", "file_id": "some-file-id",
1012 }]}
1013 else:
1014 raise BranchFileNotFound("dummy", dirname)
1015
1016 self.useFixture(BranchHostingFixture(
1017 blob=b"name: test-snap")).getInventory = getInventory
1018 branch = self.factory.makeBranch()
1019 self.assertEqual(
1020 {"name": "test-snap"},
1021 getUtility(ISnapSet).getSnapcraftYaml(branch))
1022
1023 def test_getSnapcraftYaml_bzr_dot_snapcraft_yaml(self):
1024 def getInventory(unique_name, dirname, *args, **kwargs):
1025 if dirname == "":
1026 return {"filelist": [{
1027 "filename": ".snapcraft.yaml", "file_id": "some-file-id",
1028 }]}
1029 else:
1030 raise BranchFileNotFound("dummy", dirname)
1031
1032 self.useFixture(BranchHostingFixture(
1033 blob=b"name: test-snap")).getInventory = getInventory
1034 branch = self.factory.makeBranch()
1035 self.assertEqual(
1036 {"name": "test-snap"},
1037 getUtility(ISnapSet).getSnapcraftYaml(branch))
1038
1039 def test_getSnapcraftYaml_bzr_error(self):
1040 self.useFixture(BranchHostingFixture()).getInventory = FakeMethod(
1041 failure=BranchHostingFault)
1042 branch = self.factory.makeBranch()
1043 self.assertRaises(
1044 CannotFetchSnapcraftYaml,
1045 getUtility(ISnapSet).getSnapcraftYaml, branch)
1046
1047 def test_getSnapcraftYaml_git_snap_snapcraft_yaml(self):
1048 def getBlob(path, filename, *args, **kwargs):
1049 if filename == "snap/snapcraft.yaml":
1050 return b"name: test-snap"
1051 else:
1052 raise GitRepositoryBlobNotFound("dummy", filename)
1053
1054 self.useFixture(GitHostingFixture()).getBlob = getBlob
1055 [git_ref] = self.factory.makeGitRefs()
1056 self.assertEqual(
1057 {"name": "test-snap"},
1058 getUtility(ISnapSet).getSnapcraftYaml(git_ref))
1059
1060 def test_getSnapcraftYaml_git_plain_snapcraft_yaml(self):
1061 def getBlob(path, filename, *args, **kwargs):
1062 if filename == "snapcraft.yaml":
1063 return b"name: test-snap"
1064 else:
1065 raise GitRepositoryBlobNotFound("dummy", filename)
1066
1067 self.useFixture(GitHostingFixture()).getBlob = getBlob
1068 [git_ref] = self.factory.makeGitRefs()
1069 self.assertEqual(
1070 {"name": "test-snap"},
1071 getUtility(ISnapSet).getSnapcraftYaml(git_ref))
1072
1073 def test_getSnapcraftYaml_git_dot_snapcraft_yaml(self):
1074 def getBlob(path, filename, *args, **kwargs):
1075 if filename == ".snapcraft.yaml":
1076 return b"name: test-snap"
1077 else:
1078 raise GitRepositoryBlobNotFound("dummy", filename)
1079
1080 self.useFixture(GitHostingFixture()).getBlob = getBlob
1081 [git_ref] = self.factory.makeGitRefs()
1082 self.assertEqual(
1083 {"name": "test-snap"},
1084 getUtility(ISnapSet).getSnapcraftYaml(git_ref))
1085
1086 def test_getSnapcraftYaml_git_error(self):
1087 self.useFixture(GitHostingFixture()).getBlob = FakeMethod(
1088 failure=GitRepositoryScanFault)
1089 [git_ref] = self.factory.makeGitRefs()
1090 self.assertRaises(
1091 CannotFetchSnapcraftYaml,
1092 getUtility(ISnapSet).getSnapcraftYaml, git_ref)
1093
1094 def test_getSnapcraftYaml_snap_bzr(self):
1095 self.useFixture(BranchHostingFixture(
1096 file_list={"snapcraft.yaml": "some-file-id"},
1097 blob=b"name: test-snap"))
1098 branch = self.factory.makeBranch()
1099 snap = self.factory.makeSnap(branch=branch)
1100 self.assertEqual(
1101 {"name": "test-snap"}, getUtility(ISnapSet).getSnapcraftYaml(snap))
1102
1103 def test_getSnapcraftYaml_snap_git(self):
1104 self.useFixture(GitHostingFixture(blob=b"name: test-snap"))
1105 [git_ref] = self.factory.makeGitRefs()
1106 snap = self.factory.makeSnap(git_ref=git_ref)
1107 self.assertEqual(
1108 {"name": "test-snap"}, getUtility(ISnapSet).getSnapcraftYaml(snap))
1109
1110 def test_getSnapcraftYaml_invalid_data(self):
1111 hosting_fixture = self.useFixture(GitHostingFixture())
1112 for invalid_result in (None, 123, b"", b"[][]", b"#name:test", b"]"):
1113 [git_ref] = self.factory.makeGitRefs()
1114 hosting_fixture.getBlob = FakeMethod(result=invalid_result)
1115 self.assertRaises(
1116 CannotParseSnapcraftYaml,
1117 getUtility(ISnapSet).getSnapcraftYaml, git_ref)
1118
1119 def test_getSnapcraftYaml_safe_yaml(self):
1120 self.useFixture(GitHostingFixture(blob=b"Malicious YAML!"))
1121 [git_ref] = self.factory.makeGitRefs()
1122 unsafe_load = self.useFixture(MockPatch("yaml.load"))
1123 safe_load = self.useFixture(MockPatch("yaml.safe_load"))
1124 self.assertRaises(
1125 CannotParseSnapcraftYaml,
1126 getUtility(ISnapSet).getSnapcraftYaml, git_ref)
1127 self.assertEqual(0, unsafe_load.mock.call_count)
1128 self.assertEqual(1, safe_load.mock.call_count)
1129
978 def test__findStaleSnaps(self):1130 def test__findStaleSnaps(self):
979 # Stale; not built automatically.1131 # Stale; not built automatically.
980 self.factory.makeSnap(is_stale=True)1132 self.factory.makeSnap(is_stale=True)