Merge lp:~cjwatson/launchpad/snap-git-url into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18288
Proposed branch: lp:~cjwatson/launchpad/snap-git-url
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/git-ref-remote
Diff against target: 574 lines (+269/-23)
9 files modified
lib/lp/snappy/browser/snap.py (+1/-1)
lib/lp/snappy/browser/tests/test_snap.py (+50/-0)
lib/lp/snappy/browser/tests/test_snaplisting.py (+12/-5)
lib/lp/snappy/interfaces/snap.py (+37/-5)
lib/lp/snappy/model/snap.py (+60/-5)
lib/lp/snappy/model/snapbuildbehaviour.py (+6/-2)
lib/lp/snappy/tests/test_snap.py (+76/-1)
lib/lp/snappy/tests/test_snapbuildbehaviour.py (+24/-2)
lib/lp/testing/matchers.py (+3/-2)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-git-url
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+312348@code.launchpad.net

Commit message

Allow building snaps from an external Git repository.

Description of the change

The UI for this works to the extent that it displays something approximately reasonable and lets you edit such snaps, but the only reasonable way to create them is via the API [1] since the +new-snap view is based on Branch or GitRef. That's OK for the moment since the immediate audience for this is the separate build.snapcraft.io service.

https://code.launchpad.net/~cjwatson/launchpad-buildd/snap-proxy-allow-repo/+merge/312138 will need to be deployed before dispatching builds based on external repositories will work.

[1] Technically, you can create one based on some LP-hosted repository and then edit it into the shape you want. I don't count that as a reasonable way.

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/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 2016-11-07 18:14:32 +0000
+++ lib/lp/snappy/browser/snap.py 2016-12-05 19:02:57 +0000
@@ -700,7 +700,7 @@
700 custom_widget('store_distro_series', LaunchpadRadioWidget)700 custom_widget('store_distro_series', LaunchpadRadioWidget)
701 custom_widget('store_channels', LabeledMultiCheckBoxWidget)701 custom_widget('store_channels', LabeledMultiCheckBoxWidget)
702 custom_widget('vcs', LaunchpadRadioWidget)702 custom_widget('vcs', LaunchpadRadioWidget)
703 custom_widget('git_ref', GitRefWidget)703 custom_widget('git_ref', GitRefWidget, allow_external=True)
704 custom_widget('auto_build_archive', SnapArchiveWidget)704 custom_widget('auto_build_archive', SnapArchiveWidget)
705 custom_widget('auto_build_pocket', LaunchpadDropdownWidget)705 custom_widget('auto_build_pocket', LaunchpadDropdownWidget)
706706
707707
=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py 2016-11-07 18:14:32 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py 2016-12-05 19:02:57 +0000
@@ -724,6 +724,29 @@
724 "name.",724 "name.",
725 extract_text(find_tags_by_class(browser.contents, "message")[1]))725 extract_text(find_tags_by_class(browser.contents, "message")[1]))
726726
727 def test_edit_snap_git_url(self):
728 series = self.factory.makeUbuntuDistroSeries()
729 with admin_logged_in():
730 snappy_series = self.factory.makeSnappySeries(
731 usable_distro_series=[series])
732 old_ref = self.factory.makeGitRefRemote()
733 new_ref = self.factory.makeGitRefRemote()
734 new_repository_url = new_ref.repository_url
735 new_path = new_ref.path
736 snap = self.factory.makeSnap(
737 registrant=self.person, owner=self.person, distroseries=series,
738 git_ref=old_ref, store_series=snappy_series)
739 browser = self.getViewBrowser(snap, user=self.person)
740 browser.getLink("Edit snap package").click()
741 browser.getControl("Git repository").value = new_repository_url
742 browser.getControl("Git branch").value = new_path
743 browser.getControl("Update snap package").click()
744 login_person(self.person)
745 content = find_main_content(browser.contents)
746 self.assertThat(
747 "Source:\n%s\nEdit snap package" % new_ref.display_name,
748 MatchesTagText(content, "source"))
749
727 def setUpDistroSeries(self):750 def setUpDistroSeries(self):
728 """Set up a distroseries with some available processors."""751 """Set up a distroseries with some available processors."""
729 distroseries = self.factory.makeUbuntuDistroSeries()752 distroseries = self.factory.makeUbuntuDistroSeries()
@@ -1259,6 +1282,33 @@
1259 Primary Archive for Ubuntu Linux1282 Primary Archive for Ubuntu Linux
1260 """, self.getMainText(build.snap))1283 """, self.getMainText(build.snap))
12611284
1285 def test_index_git_url(self):
1286 ref = self.factory.makeGitRefRemote(
1287 repository_url=u"https://git.example.org/foo",
1288 path=u"refs/heads/master")
1289 snap = self.makeSnap(git_ref=ref)
1290 build = self.makeBuild(
1291 snap=snap, status=BuildStatus.FULLYBUILT,
1292 duration=timedelta(minutes=30))
1293 self.assertTextMatchesExpressionIgnoreWhitespace("""\
1294 Snap packages snap-name
1295 .*
1296 Snap package information
1297 Owner: Test Person
1298 Distribution series: Ubuntu Shiny
1299 Source: https://git.example.org/foo master
1300 Build schedule: \(\?\)
1301 Built on request
1302 Source archive for automatic builds:
1303 Pocket for automatic builds:
1304 Builds of this snap package are not automatically uploaded to
1305 the store.
1306 Latest builds
1307 Status When complete Architecture Archive
1308 Successfully built 30 minutes ago i386
1309 Primary Archive for Ubuntu Linux
1310 """, self.getMainText(build.snap))
1311
1262 def test_index_success_with_buildlog(self):1312 def test_index_success_with_buildlog(self):
1263 # The build log is shown if it is there.1313 # The build log is shown if it is there.
1264 build = self.makeBuild(1314 build = self.makeBuild(
12651315
=== modified file 'lib/lp/snappy/browser/tests/test_snaplisting.py'
--- lib/lp/snappy/browser/tests/test_snaplisting.py 2016-09-07 11:12:58 +0000
+++ lib/lp/snappy/browser/tests/test_snaplisting.py 2016-12-05 19:02:57 +0000
@@ -11,6 +11,7 @@
11from lp.code.tests.helpers import GitHostingFixture11from lp.code.tests.helpers import GitHostingFixture
12from lp.services.database.constants import (12from lp.services.database.constants import (
13 ONE_DAY_AGO,13 ONE_DAY_AGO,
14 SEVEN_DAYS_AGO,
14 UTC_NOW,15 UTC_NOW,
15 )16 )
16from lp.services.webapp import canonical_url17from lp.services.webapp import canonical_url
@@ -107,16 +108,22 @@
107 owner = self.factory.makePerson(displayname="Snap Owner")108 owner = self.factory.makePerson(displayname="Snap Owner")
108 self.factory.makeSnap(109 self.factory.makeSnap(
109 registrant=owner, owner=owner, branch=self.factory.makeAnyBranch(),110 registrant=owner, owner=owner, branch=self.factory.makeAnyBranch(),
111 date_created=SEVEN_DAYS_AGO)
112 [ref] = self.factory.makeGitRefs()
113 self.factory.makeSnap(
114 registrant=owner, owner=owner, git_ref=ref,
110 date_created=ONE_DAY_AGO)115 date_created=ONE_DAY_AGO)
111 [ref] = self.factory.makeGitRefs()116 remote_ref = self.factory.makeGitRefRemote()
112 self.factory.makeSnap(117 self.factory.makeSnap(
113 registrant=owner, owner=owner, git_ref=ref, date_created=UTC_NOW)118 registrant=owner, owner=owner, git_ref=remote_ref,
119 date_created=UTC_NOW)
114 text = self.getMainText(owner, "+snaps")120 text = self.getMainText(owner, "+snaps")
115 self.assertTextMatchesExpressionIgnoreWhitespace("""121 self.assertTextMatchesExpressionIgnoreWhitespace("""
116 Snap packages for Snap Owner122 Snap packages for Snap Owner
117 Name Source Registered123 Name Source Registered
118 snap-name.* ~.*:.* .*124 snap-name.* http://.* path-.* .*
119 snap-name.* lp:.* .*""", text)125 snap-name.* ~.*:.* .*
126 snap-name.* lp:.* .*""", text)
120127
121 def test_project_snap_listing(self):128 def test_project_snap_listing(self):
122 # We can see snap packages for a project.129 # We can see snap packages for a project.
123130
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py 2016-11-22 02:13:11 +0000
+++ lib/lp/snappy/interfaces/snap.py 2016-12-05 19:02:57 +0000
@@ -85,6 +85,7 @@
85from lp.services.fields import (85from lp.services.fields import (
86 PersonChoice,86 PersonChoice,
87 PublicPersonChoice,87 PublicPersonChoice,
88 URIField,
88 )89 )
89from lp.services.webhooks.interfaces import IWebhookTarget90from lp.services.webhooks.interfaces import IWebhookTarget
90from lp.snappy.interfaces.snappyseries import (91from lp.snappy.interfaces.snappyseries import (
@@ -380,6 +381,18 @@
380 "A Git repository with a branch containing a snapcraft.yaml "381 "A Git repository with a branch containing a snapcraft.yaml "
381 "recipe at the top level.")))382 "recipe at the top level.")))
382383
384 git_repository_url = exported(URIField(
385 title=_("Git repository URL"), required=False, readonly=True,
386 description=_(
387 "The URL of a Git repository with a branch containing a "
388 "snapcraft.yaml recipe at the top level."),
389 allowed_schemes=["git", "http", "https"],
390 allow_userinfo=True,
391 allow_port=True,
392 allow_query=False,
393 allow_fragment=False,
394 trailing_slash=False))
395
383 git_path = exported(TextLine(396 git_path = exported(TextLine(
384 title=_("Git branch path"), required=False, readonly=True,397 title=_("Git branch path"), required=False, readonly=True,
385 description=_(398 description=_(
@@ -503,11 +516,13 @@
503 @export_factory_operation(516 @export_factory_operation(
504 ISnap, [517 ISnap, [
505 "owner", "distro_series", "name", "description", "branch",518 "owner", "distro_series", "name", "description", "branch",
506 "git_ref", "auto_build", "auto_build_archive", "auto_build_pocket",519 "git_repository", "git_repository_url", "git_path", "git_ref",
520 "auto_build", "auto_build_archive", "auto_build_pocket",
507 "private"])521 "private"])
508 @operation_for_version("devel")522 @operation_for_version("devel")
509 def new(registrant, owner, distro_series, name, description=None,523 def new(registrant, owner, distro_series, name, description=None,
510 branch=None, git_ref=None, auto_build=False,524 branch=None, git_repository=None, git_repository_url=None,
525 git_path=None, git_ref=None, auto_build=False,
511 auto_build_archive=None, auto_build_pocket=None,526 auto_build_archive=None, auto_build_pocket=None,
512 require_virtualized=True, processors=None, date_created=None,527 require_virtualized=True, processors=None, date_created=None,
513 private=False, store_upload=False, store_series=None,528 private=False, store_upload=False, store_series=None,
@@ -540,7 +555,7 @@
540555
541 :param person: An `IPerson`.556 :param person: An `IPerson`.
542 :param visible_by_user: If not None, only return packages visible by557 :param visible_by_user: If not None, only return packages visible by
543 this user.558 this user; otherwise, only return publicly-visible packages.
544 """559 """
545560
546 def findByProject(project, visible_by_user=None):561 def findByProject(project, visible_by_user=None):
@@ -548,7 +563,7 @@
548563
549 :param project: An `IProduct`.564 :param project: An `IProduct`.
550 :param visible_by_user: If not None, only return packages visible by565 :param visible_by_user: If not None, only return packages visible by
551 this user.566 this user; otherwise, only return publicly-visible packages.
552 """567 """
553568
554 def findByBranch(branch):569 def findByBranch(branch):
@@ -571,12 +586,29 @@
571 :param context: An `IPerson`, `IProduct, `IBranch`,586 :param context: An `IPerson`, `IProduct, `IBranch`,
572 `IGitRepository`, or `IGitRef`.587 `IGitRepository`, or `IGitRef`.
573 :param visible_by_user: If not None, only return packages visible by588 :param visible_by_user: If not None, only return packages visible by
574 this user.589 this user; otherwise, only return publicly-visible packages.
575 :param order_by_date: If True, order packages by descending590 :param order_by_date: If True, order packages by descending
576 modification date.591 modification date.
577 :raises BadSnapSearchContext: if the context is not understood.592 :raises BadSnapSearchContext: if the context is not understood.
578 """593 """
579594
595 @operation_parameters(url=TextLine(title=_("The URL to search for.")))
596 @call_with(visible_by_user=REQUEST_USER)
597 @operation_returns_collection_of(ISnap)
598 @export_read_operation()
599 @operation_for_version("devel")
600 def findByURL(url, visible_by_user=None):
601 """Return all snap packages that build from the given URL.
602
603 This currently only works for packages that build directly from a
604 URL, rather than being linked to a Bazaar branch or Git repository
605 hosted in Launchpad.
606
607 :param url: A URL.
608 :param visible_by_user: If not None, only return packages visible by
609 this user; otherwise, only return publicly-visible packages.
610 """
611
580 def preloadDataForSnaps(snaps, user):612 def preloadDataForSnaps(snaps, user):
581 """Load the data related to a list of snap packages."""613 """Load the data related to a list of snap packages."""
582614
583615
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2016-11-22 02:13:11 +0000
+++ lib/lp/snappy/model/snap.py 2016-12-05 19:02:57 +0000
@@ -17,6 +17,7 @@
17 Desc,17 Desc,
18 LeftJoin,18 LeftJoin,
19 Not,19 Not,
20 Or,
20 )21 )
21from storm.locals import (22from storm.locals import (
22 Bool,23 Bool,
@@ -54,7 +55,10 @@
54 IAllGitRepositories,55 IAllGitRepositories,
55 IGitCollection,56 IGitCollection,
56 )57 )
57from lp.code.interfaces.gitref import IGitRef58from lp.code.interfaces.gitref import (
59 IGitRef,
60 IGitRefRemoteSet,
61 )
58from lp.code.interfaces.gitrepository import IGitRepository62from lp.code.interfaces.gitrepository import IGitRepository
59from lp.code.model.branch import Branch63from lp.code.model.branch import Branch
60from lp.code.model.branchcollection import GenericBranchCollection64from lp.code.model.branchcollection import GenericBranchCollection
@@ -71,6 +75,7 @@
71 IHasOwner,75 IHasOwner,
72 IPersonRoles,76 IPersonRoles,
73 )77 )
78from lp.registry.model.teammembership import TeamParticipation
74from lp.services.config import config79from lp.services.config import config
75from lp.services.database.bulk import load_related80from lp.services.database.bulk import load_related
76from lp.services.database.constants import (81from lp.services.database.constants import (
@@ -115,6 +120,7 @@
115from lp.snappy.interfaces.snappyseries import ISnappyDistroSeriesSet120from lp.snappy.interfaces.snappyseries import ISnappyDistroSeriesSet
116from lp.snappy.model.snapbuild import SnapBuild121from lp.snappy.model.snapbuild import SnapBuild
117from lp.soyuz.interfaces.archive import ArchiveDisabled122from lp.soyuz.interfaces.archive import ArchiveDisabled
123from lp.soyuz.interfaces.buildrecords import IncompatibleArguments
118from lp.soyuz.model.archive import (124from lp.soyuz.model.archive import (
119 Archive,125 Archive,
120 get_enabled_archive_filter,126 get_enabled_archive_filter,
@@ -163,6 +169,8 @@
163 git_repository_id = Int(name='git_repository', allow_none=True)169 git_repository_id = Int(name='git_repository', allow_none=True)
164 git_repository = Reference(git_repository_id, 'GitRepository.id')170 git_repository = Reference(git_repository_id, 'GitRepository.id')
165171
172 git_repository_url = Unicode(name='git_repository_url', allow_none=True)
173
166 git_path = Unicode(name='git_path', allow_none=True)174 git_path = Unicode(name='git_path', allow_none=True)
167175
168 auto_build = Bool(name='auto_build', allow_none=False)176 auto_build = Bool(name='auto_build', allow_none=False)
@@ -226,6 +234,9 @@
226 """See `ISnap`."""234 """See `ISnap`."""
227 if self.git_repository is not None:235 if self.git_repository is not None:
228 return self.git_repository.getRefByPath(self.git_path)236 return self.git_repository.getRefByPath(self.git_path)
237 elif self.git_repository_url is not None:
238 return getUtility(IGitRefRemoteSet).new(
239 self.git_repository_url, self.git_path)
229 else:240 else:
230 return None241 return None
231242
@@ -234,9 +245,11 @@
234 """See `ISnap`."""245 """See `ISnap`."""
235 if value is not None:246 if value is not None:
236 self.git_repository = value.repository247 self.git_repository = value.repository
248 self.git_repository_url = value.repository_url
237 self.git_path = value.path249 self.git_path = value.path
238 else:250 else:
239 self.git_repository = None251 self.git_repository = None
252 self.git_repository_url = None
240 self.git_path = None253 self.git_path = None
241254
242 @property255 @property
@@ -549,7 +562,8 @@
549 """See `ISnapSet`."""562 """See `ISnapSet`."""
550563
551 def new(self, registrant, owner, distro_series, name, description=None,564 def new(self, registrant, owner, distro_series, name, description=None,
552 branch=None, git_ref=None, auto_build=False,565 branch=None, git_repository=None, git_repository_url=None,
566 git_path=None, git_ref=None, auto_build=False,
553 auto_build_archive=None, auto_build_pocket=None,567 auto_build_archive=None, auto_build_pocket=None,
554 require_virtualized=True, processors=None, date_created=DEFAULT,568 require_virtualized=True, processors=None, date_created=DEFAULT,
555 private=False, store_upload=False, store_series=None,569 private=False, store_upload=False, store_series=None,
@@ -565,6 +579,21 @@
565 "%s cannot create snap packages owned by %s." %579 "%s cannot create snap packages owned by %s." %
566 (registrant.displayname, owner.displayname))580 (registrant.displayname, owner.displayname))
567581
582 if sum([git_repository is not None, git_repository_url is not None,
583 git_ref is not None]) > 1:
584 raise IncompatibleArguments(
585 "You cannot specify more than one of 'git_repository', "
586 "'git_repository_url', and 'git_ref'.")
587 if ((git_repository is None and git_repository_url is None) !=
588 (git_path is None)):
589 raise IncompatibleArguments(
590 "You must specify both or neither of "
591 "'git_repository'/'git_repository_url' and 'git_path'.")
592 if git_repository is not None:
593 git_ref = git_repository.getRefByPath(git_path)
594 elif git_repository_url is not None:
595 git_ref = getUtility(IGitRefRemoteSet).new(
596 git_repository_url, git_path)
568 if branch is None and git_ref is None:597 if branch is None and git_ref is None:
569 raise NoSourceForSnap598 raise NoSourceForSnap
570 if self.exists(owner, name):599 if self.exists(owner, name):
@@ -603,8 +632,8 @@
603 return True632 return True
604633
605 # Public snaps with private sources are not allowed.634 # Public snaps with private sources are not allowed.
606 source_ref = branch or git_ref635 source = branch or git_ref
607 if source_ref.information_type in PRIVATE_INFORMATION_TYPES:636 if source.information_type in PRIVATE_INFORMATION_TYPES:
608 return False637 return False
609638
610 # Public snaps owned by private teams are not allowed.639 # Public snaps owned by private teams are not allowed.
@@ -653,8 +682,12 @@
653 return owned.union(packaged)682 return owned.union(packaged)
654683
655 bzr_collection = removeSecurityProxy(getUtility(IAllBranches))684 bzr_collection = removeSecurityProxy(getUtility(IAllBranches))
685 bzr_snaps = _getSnaps(bzr_collection)
656 git_collection = removeSecurityProxy(getUtility(IAllGitRepositories))686 git_collection = removeSecurityProxy(getUtility(IAllGitRepositories))
657 return _getSnaps(bzr_collection).union(_getSnaps(git_collection))687 git_snaps = _getSnaps(git_collection)
688 git_url_snaps = IStore(Snap).find(
689 Snap, Snap.owner == person, Snap.git_repository_url != None)
690 return bzr_snaps.union(git_snaps).union(git_url_snaps)
658691
659 def findByProject(self, project, visible_by_user=None):692 def findByProject(self, project, visible_by_user=None):
660 """See `ISnapSet`."""693 """See `ISnapSet`."""
@@ -705,6 +738,28 @@
705 snaps.order_by(Desc(Snap.date_last_modified))738 snaps.order_by(Desc(Snap.date_last_modified))
706 return snaps739 return snaps
707740
741 def findByURL(self, url, visible_by_user=None):
742 """See `ISnapSet`."""
743 clauses = [Snap.git_repository_url == url]
744 # XXX cjwatson 2016-11-25: This is in principle a poor query, but we
745 # don't yet have the access grant infrastructure to do better, and
746 # in any case since we're querying for a single URL the numbers
747 # involved should be very small.
748 if visible_by_user is None:
749 visibility_clause = Snap.private == False
750 else:
751 roles = IPersonRoles(visible_by_user)
752 if roles.in_admin or roles.in_commercial_admin:
753 visibility_clause = True
754 else:
755 visibility_clause = Or(
756 Snap.private == False,
757 And(
758 TeamParticipation.person == visible_by_user,
759 TeamParticipation.teamID == Snap.owner_id))
760 clauses.append(visibility_clause)
761 return IStore(Snap).find(Snap, *clauses).config(distinct=True)
762
708 def preloadDataForSnaps(self, snaps, user=None):763 def preloadDataForSnaps(self, snaps, user=None):
709 """See `ISnapSet`."""764 """See `ISnapSet`."""
710 snaps = [removeSecurityProxy(snap) for snap in snaps]765 snaps = [removeSecurityProxy(snap) for snap in snaps]
711766
=== modified file 'lib/lp/snappy/model/snapbuildbehaviour.py'
--- lib/lp/snappy/model/snapbuildbehaviour.py 2016-09-21 02:50:41 +0000
+++ lib/lp/snappy/model/snapbuildbehaviour.py 2016-12-05 19:02:57 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""An `IBuildFarmJobBehaviour` for `SnapBuild`.4"""An `IBuildFarmJobBehaviour` for `SnapBuild`.
@@ -105,7 +105,11 @@
105 if build.snap.branch is not None:105 if build.snap.branch is not None:
106 args["branch"] = build.snap.branch.bzr_identity106 args["branch"] = build.snap.branch.bzr_identity
107 elif build.snap.git_ref is not None:107 elif build.snap.git_ref is not None:
108 args["git_repository"] = build.snap.git_repository.git_https_url108 if build.snap.git_ref.repository_url is not None:
109 args["git_repository"] = build.snap.git_ref.repository_url
110 else:
111 args["git_repository"] = (
112 build.snap.git_repository.git_https_url)
109 # "git clone -b" doesn't accept full ref names. If this becomes113 # "git clone -b" doesn't accept full ref names. If this becomes
110 # a problem then we could change launchpad-buildd to do "git114 # a problem then we could change launchpad-buildd to do "git
111 # clone" followed by "git checkout" instead.115 # clone" followed by "git checkout" instead.
112116
=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py 2016-11-22 02:13:11 +0000
+++ lib/lp/snappy/tests/test_snap.py 2016-12-05 19:02:57 +0000
@@ -87,7 +87,10 @@
87 DoesNotSnapshot,87 DoesNotSnapshot,
88 HasQueryCount,88 HasQueryCount,
89 )89 )
90from lp.testing.pages import webservice_for_person90from lp.testing.pages import (
91 LaunchpadWebServiceCaller,
92 webservice_for_person,
93 )
9194
9295
93class TestSnapFeatureFlag(TestCaseWithFactory):96class TestSnapFeatureFlag(TestCaseWithFactory):
@@ -591,6 +594,18 @@
591 self.assertTrue(snap.require_virtualized)594 self.assertTrue(snap.require_virtualized)
592 self.assertFalse(snap.private)595 self.assertFalse(snap.private)
593596
597 def test_creation_git_url(self):
598 # A Snap can be backed directly by a URL for an external Git
599 # repository, rather than a Git repository hosted in Launchpad.
600 ref = self.factory.makeGitRefRemote()
601 components = self.makeSnapComponents(git_ref=ref)
602 snap = getUtility(ISnapSet).new(**components)
603 self.assertIsNone(snap.branch)
604 self.assertIsNone(snap.git_repository)
605 self.assertEqual(ref.repository_url, snap.git_repository_url)
606 self.assertEqual(ref.path, snap.git_path)
607 self.assertEqual(ref, snap.git_ref)
608
594 def test_private_snap_for_public_sources(self):609 def test_private_snap_for_public_sources(self):
595 # Creating private snaps for public sources is allowed.610 # Creating private snaps for public sources is allowed.
596 [ref] = self.factory.makeGitRefs()611 [ref] = self.factory.makeGitRefs()
@@ -803,6 +818,20 @@
803 BadSnapSearchContext, snap_set.findByContext,818 BadSnapSearchContext, snap_set.findByContext,
804 self.factory.makeDistribution())819 self.factory.makeDistribution())
805820
821 def test_findByURL(self):
822 # ISnapSet.findByURL returns visible Snaps with the given URL.
823 urls = [u"https://git.example.org/foo", u"https://git.example.org/bar"]
824 snaps = []
825 for url in urls:
826 snaps.append(self.factory.makeSnap(
827 git_ref=self.factory.makeGitRefRemote(repository_url=url)))
828 snaps.append(
829 self.factory.makeSnap(branch=self.factory.makeAnyBranch()))
830 snaps.append(
831 self.factory.makeSnap(git_ref=self.factory.makeGitRefs()[0]))
832 self.assertContentEqual(
833 [snaps[0]], getUtility(ISnapSet).findByURL(urls[0]))
834
806 def test__findStaleSnaps(self):835 def test__findStaleSnaps(self):
807 # Stale; not built automatically.836 # Stale; not built automatically.
808 self.factory.makeSnap(is_stale=True)837 self.factory.makeSnap(is_stale=True)
@@ -1252,6 +1281,52 @@
1252 "No such snap package with this owner: 'nonexistent'.",1281 "No such snap package with this owner: 'nonexistent'.",
1253 response.body)1282 response.body)
12541283
1284 def test_findByURL(self):
1285 # lp.snaps.findByURL returns visible Snaps with the given URL.
1286 persons = [self.factory.makePerson(), self.factory.makePerson()]
1287 urls = [u"https://git.example.org/foo", u"https://git.example.org/bar"]
1288 snaps = []
1289 for url in urls:
1290 for person in persons:
1291 for private in (False, True):
1292 ref = self.factory.makeGitRefRemote(repository_url=url)
1293 snaps.append(self.factory.makeSnap(
1294 registrant=person, git_ref=ref, private=private))
1295 with admin_logged_in():
1296 ws_snaps = [
1297 self.webservice.getAbsoluteUrl(api_url(snap))
1298 for snap in snaps]
1299 commercial_admin = (
1300 getUtility(ILaunchpadCelebrities).commercial_admin.teamowner)
1301 logout()
1302 # Anonymous requests can only see public snaps.
1303 anon_webservice = LaunchpadWebServiceCaller("test", "")
1304 response = anon_webservice.named_get(
1305 "/+snaps", "findByURL", url=urls[0], api_version="devel")
1306 self.assertEqual(200, response.status)
1307 self.assertContentEqual(
1308 [ws_snaps[0], ws_snaps[2]],
1309 [entry["self_link"] for entry in response.jsonBody()["entries"]])
1310 # persons[0] can see both public snaps with this URL, as well as
1311 # their own private snap.
1312 webservice = webservice_for_person(
1313 persons[0], permission=OAuthPermission.READ_PRIVATE)
1314 response = webservice.named_get(
1315 "/+snaps", "findByURL", url=urls[0], api_version="devel")
1316 self.assertEqual(200, response.status)
1317 self.assertContentEqual(
1318 ws_snaps[:3],
1319 [entry["self_link"] for entry in response.jsonBody()["entries"]])
1320 # Admins can see all snaps with this URL.
1321 commercial_admin_webservice = webservice_for_person(
1322 commercial_admin, permission=OAuthPermission.READ_PRIVATE)
1323 response = commercial_admin_webservice.named_get(
1324 "/+snaps", "findByURL", url=urls[0], api_version="devel")
1325 self.assertEqual(200, response.status)
1326 self.assertContentEqual(
1327 ws_snaps[:4],
1328 [entry["self_link"] for entry in response.jsonBody()["entries"]])
1329
1255 def setProcessors(self, user, snap, names):1330 def setProcessors(self, user, snap, names):
1256 ws = webservice_for_person(1331 ws = webservice_for_person(
1257 user, permission=OAuthPermission.WRITE_PUBLIC)1332 user, permission=OAuthPermission.WRITE_PUBLIC)
12581333
=== modified file 'lib/lp/snappy/tests/test_snapbuildbehaviour.py'
--- lib/lp/snappy/tests/test_snapbuildbehaviour.py 2016-06-28 21:10:18 +0000
+++ lib/lp/snappy/tests/test_snapbuildbehaviour.py 2016-12-05 19:02:57 +0000
@@ -11,13 +11,13 @@
1111
12import fixtures12import fixtures
13from mock import (13from mock import (
14 Mock,
14 patch,15 patch,
15 Mock,
16 )16 )
17import transaction
18from testtools import ExpectedException17from testtools import ExpectedException
19from testtools.deferredruntest import AsynchronousDeferredRunTest18from testtools.deferredruntest import AsynchronousDeferredRunTest
20from testtools.matchers import IsInstance19from testtools.matchers import IsInstance
20import transaction
21from twisted.internet import defer21from twisted.internet import defer
22from twisted.trial.unittest import TestCase as TrialTestCase22from twisted.trial.unittest import TestCase as TrialTestCase
23from zope.component import getUtility23from zope.component import getUtility
@@ -244,6 +244,28 @@
244 }, args)244 }, args)
245245
246 @defer.inlineCallbacks246 @defer.inlineCallbacks
247 def test_extraBuildArgs_git_url(self):
248 # _extraBuildArgs returns appropriate arguments if asked to build a
249 # job for a Git branch backed by a URL for an external repository.
250 url = u"https://git.example.org/foo"
251 ref = self.factory.makeGitRefRemote(
252 repository_url=url, path=u"refs/heads/master")
253 job = self.makeJob(git_ref=ref)
254 expected_archives = get_sources_list_for_building(
255 job.build, job.build.distro_arch_series, None)
256 args = yield job._extraBuildArgs()
257 self.assertEqual({
258 "archive_private": False,
259 "archives": expected_archives,
260 "arch_tag": "i386",
261 "git_repository": url,
262 "git_path": "master",
263 "name": u"test-snap",
264 "proxy_url": self.proxy_url,
265 "revocation_endpoint": self.revocation_endpoint,
266 }, args)
267
268 @defer.inlineCallbacks
247 def test_extraBuildArgs_proxy_url_set(self):269 def test_extraBuildArgs_proxy_url_set(self):
248 job = self.makeJob()270 job = self.makeJob()
249 build_request = yield job.composeBuildRequest(None)271 build_request = yield job.composeBuildRequest(None)
250272
=== modified file 'lib/lp/testing/matchers.py'
--- lib/lp/testing/matchers.py 2015-10-13 16:58:20 +0000
+++ lib/lp/testing/matchers.py 2016-12-05 19:02:57 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the1# Copyright 2010-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -395,7 +395,8 @@
395 self.soup_content = soup_content395 self.soup_content = soup_content
396396
397 def get_details(self):397 def get_details(self):
398 return {'content': self.soup_content}398 content = unicode(self.soup_content).encode('utf8')
399 return {'content': Content(UTF8_TEXT, lambda: [content])}
399400
400401
401class MissingElement(SoupMismatch):402class MissingElement(SoupMismatch):