Merge ~cjwatson/launchpad:dominator-channel-map into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 038e321722bbc735367ae64ce92dac32aa528f80
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:dominator-channel-map
Merge into: launchpad:master
Diff against target: 1347 lines (+456/-124)
14 files modified
lib/lp/archivepublisher/domination.py (+73/-26)
lib/lp/archivepublisher/publishing.py (+3/-0)
lib/lp/archivepublisher/tests/test_dominator.py (+74/-9)
lib/lp/code/interfaces/cibuild.py (+8/-0)
lib/lp/code/model/cibuild.py (+13/-0)
lib/lp/code/model/tests/test_cibuild.py (+19/-0)
lib/lp/soyuz/adapters/packagelocation.py (+27/-5)
lib/lp/soyuz/adapters/tests/test_packagelocation.py (+20/-2)
lib/lp/soyuz/enums.py (+14/-0)
lib/lp/soyuz/interfaces/publishing.py (+6/-8)
lib/lp/soyuz/model/binarypackagerelease.py (+2/-0)
lib/lp/soyuz/model/publishing.py (+78/-33)
lib/lp/soyuz/tests/test_publishing.py (+70/-20)
lib/lp/testing/factory.py (+49/-21)
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+422233@code.launchpad.net

Commit message

Support dominating publications by channel

Description of the change

This means that, once we start publishing binaries to snap-store-style semantic channels (e.g. "stable", "1.0/candidate", etc.), the dominator will be able to correctly supersede publications that have been replaced with a different version of the same package in the same channel.

I had to prepare for this by adding channel support to various publishing primitives. There's no UI or API support for any of this yet, but I at least needed enough to support the dominator and its tests.

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
diff --git a/lib/lp/archivepublisher/domination.py b/lib/lp/archivepublisher/domination.py
index 5cc9422..526c8ea 100644
--- a/lib/lp/archivepublisher/domination.py
+++ b/lib/lp/archivepublisher/domination.py
@@ -64,6 +64,7 @@ from storm.expr import (
64 And,64 And,
65 Count,65 Count,
66 Desc,66 Desc,
67 Not,
67 Select,68 Select,
68 )69 )
69from zope.component import getUtility70from zope.component import getUtility
@@ -74,7 +75,9 @@ from lp.services.database.constants import UTC_NOW
74from lp.services.database.decoratedresultset import DecoratedResultSet75from lp.services.database.decoratedresultset import DecoratedResultSet
75from lp.services.database.interfaces import IStore76from lp.services.database.interfaces import IStore
76from lp.services.database.sqlbase import flush_database_updates77from lp.services.database.sqlbase import flush_database_updates
78from lp.services.database.stormexpr import IsDistinctFrom
77from lp.services.orderingcheck import OrderingCheck79from lp.services.orderingcheck import OrderingCheck
80from lp.soyuz.adapters.packagelocation import PackageLocation
78from lp.soyuz.enums import (81from lp.soyuz.enums import (
79 BinaryPackageFormat,82 BinaryPackageFormat,
80 PackagePublishingStatus,83 PackagePublishingStatus,
@@ -204,6 +207,17 @@ class GeneralizedPublication:
204 return sorted(publications, key=cmp_to_key(self.compare), reverse=True)207 return sorted(publications, key=cmp_to_key(self.compare), reverse=True)
205208
206209
210def make_package_location(pub):
211 """Make a `PackageLocation` representing a publication."""
212 return PackageLocation(
213 archive=pub.archive,
214 distribution=pub.distroseries.distribution,
215 distroseries=pub.distroseries,
216 pocket=pub.pocket,
217 channel=pub.channel,
218 )
219
220
207def find_live_source_versions(sorted_pubs):221def find_live_source_versions(sorted_pubs):
208 """Find versions out of Published publications that should stay live.222 """Find versions out of Published publications that should stay live.
209223
@@ -336,10 +350,19 @@ def find_live_binary_versions_pass_2(sorted_pubs, cache):
336 [pub.binarypackagerelease for pub in arch_indep_pubs], ['buildID'])350 [pub.binarypackagerelease for pub in arch_indep_pubs], ['buildID'])
337 load_related(SourcePackageRelease, bpbs, ['source_package_release_id'])351 load_related(SourcePackageRelease, bpbs, ['source_package_release_id'])
338352
353 # XXX cjwatson 2022-05-01: Skip the architecture-specific check for
354 # publications from CI builds for now, until we figure out how to
355 # approximate source package releases for groups of CI builds. We don't
356 # currently expect problematic situations to come up on production; CI
357 # builds are currently only expected to be used in situations where
358 # either we don't build both architecture-specific and
359 # architecture-independent packages, or where tight dependencies between
360 # the two aren't customary.
339 reprieved_pubs = [361 reprieved_pubs = [
340 pub362 pub
341 for pub in arch_indep_pubs363 for pub in arch_indep_pubs
342 if cache.hasArchSpecificPublications(pub)]364 if pub.binarypackagerelease.ci_build_id is None and
365 cache.hasArchSpecificPublications(pub)]
343366
344 return get_binary_versions([latest] + arch_specific_pubs + reprieved_pubs)367 return get_binary_versions([latest] + arch_specific_pubs + reprieved_pubs)
345368
@@ -382,9 +405,9 @@ class Dominator:
382 list we import.405 list we import.
383406
384 :param sorted_pubs: A list of publications for the same package,407 :param sorted_pubs: A list of publications for the same package,
385 in the same archive, series, and pocket, all with status408 in the same archive, series, pocket, and channel, all with
386 `PackagePublishingStatus.PUBLISHED`. They must be sorted from409 status `PackagePublishingStatus.PUBLISHED`. They must be sorted
387 most current to least current, as would be the result of410 from most current to least current, as would be the result of
388 `generalization.sortPublications`.411 `generalization.sortPublications`.
389 :param live_versions: Iterable of versions that are still considered412 :param live_versions: Iterable of versions that are still considered
390 "live" for this package. For any of these, the latest publication413 "live" for this package. For any of these, the latest publication
@@ -458,31 +481,47 @@ class Dominator:
458 return supersede, keep, delete481 return supersede, keep, delete
459482
460 def _sortPackages(self, publications, generalization):483 def _sortPackages(self, publications, generalization):
461 """Partition publications by package name, and sort them.484 """Partition publications by package name and location, and sort them.
462485
463 The publications are sorted from most current to least current,486 The publications are sorted from most current to least current,
464 as required by `planPackageDomination` etc.487 as required by `planPackageDomination` etc. Locations are currently
488 (package name, channel).
465489
466 :param publications: An iterable of `SourcePackagePublishingHistory`490 :param publications: An iterable of `SourcePackagePublishingHistory`
467 or of `BinaryPackagePublishingHistory`.491 or of `BinaryPackagePublishingHistory`.
468 :param generalization: A `GeneralizedPublication` helper representing492 :param generalization: A `GeneralizedPublication` helper representing
469 the kind of publications these are: source or binary.493 the kind of publications these are: source or binary.
470 :return: A dict mapping each package name to a sorted list of494 :return: A dict mapping each package location (package name,
471 publications from `publications`.495 channel) to a sorted list of publications from `publications`.
472 """496 """
473 pubs_by_package = defaultdict(list)497 # XXX cjwatson 2022-05-19: Traditional suites (distroseries/pocket)
498 # are divided up in the loop in Publisher.B_dominate. However, this
499 # doesn't scale to channel-map-style suites (distroseries/channel),
500 # since there may be a very large number of channels and we don't
501 # want to have to loop over all the possible ones, so we divide
502 # those up here instead.
503 #
504 # This is definitely confusing. In the longer term, we should
505 # probably push the loop down from the publisher to here, and sort
506 # and dominate all candidates in a given archive at once: there's no
507 # particularly obvious reason not to, and it might perform better as
508 # well.
509
510 pubs_by_name_and_location = defaultdict(list)
474 for pub in publications:511 for pub in publications:
475 pubs_by_package[generalization.getPackageName(pub)].append(pub)512 name = generalization.getPackageName(pub)
513 location = make_package_location(pub)
514 pubs_by_name_and_location[(name, location)].append(pub)
476515
477 # Sort the publication lists. This is not an in-place sort, so516 # Sort the publication lists. This is not an in-place sort, so
478 # it involves altering the dict while we iterate it. Listify517 # it involves altering the dict while we iterate it. Listify
479 # the keys so that we can be sure that we're not altering the518 # the items so that we can be sure that we're not altering the
480 # iteration order while iteration is underway.519 # iteration order while iteration is underway.
481 for package in list(pubs_by_package.keys()):520 for (name, location), pubs in list(pubs_by_name_and_location.items()):
482 pubs_by_package[package] = generalization.sortPublications(521 pubs_by_name_and_location[(name, location)] = (
483 pubs_by_package[package])522 generalization.sortPublications(pubs))
484523
485 return pubs_by_package524 return pubs_by_name_and_location
486525
487 def _setScheduledDeletionDate(self, pub_record):526 def _setScheduledDeletionDate(self, pub_record):
488 """Set the scheduleddeletiondate on a publishing record.527 """Set the scheduleddeletiondate on a publishing record.
@@ -541,7 +580,10 @@ class Dominator:
541 BinaryPackagePublishingHistory.binarypackagerelease ==580 BinaryPackagePublishingHistory.binarypackagerelease ==
542 BinaryPackageRelease.id,581 BinaryPackageRelease.id,
543 BinaryPackageRelease.build == BinaryPackageBuild.id,582 BinaryPackageRelease.build == BinaryPackageBuild.id,
544 BinaryPackagePublishingHistory.pocket == pub_record.pocket)583 BinaryPackagePublishingHistory.pocket == pub_record.pocket,
584 Not(IsDistinctFrom(
585 BinaryPackagePublishingHistory._channel,
586 pub_record._channel)))
545587
546 # There is at least one non-removed binary to consider588 # There is at least one non-removed binary to consider
547 if not considered_binaries.is_empty():589 if not considered_binaries.is_empty():
@@ -552,6 +594,7 @@ class Dominator:
552 SourcePackagePublishingHistory,594 SourcePackagePublishingHistory,
553 distroseries=pub_record.distroseries,595 distroseries=pub_record.distroseries,
554 pocket=pub_record.pocket,596 pocket=pub_record.pocket,
597 channel=pub_record.channel,
555 status=PackagePublishingStatus.PUBLISHED,598 status=PackagePublishingStatus.PUBLISHED,
556 archive=self.archive,599 archive=self.archive,
557 sourcepackagerelease=srcpkg_release)600 sourcepackagerelease=srcpkg_release)
@@ -588,7 +631,8 @@ class Dominator:
588 ]631 ]
589 candidate_binary_names = Select(632 candidate_binary_names = Select(
590 BPPH.binarypackagenameID, And(*bpph_location_clauses),633 BPPH.binarypackagenameID, And(*bpph_location_clauses),
591 group_by=BPPH.binarypackagenameID, having=(Count() > 1))634 group_by=(BPPH.binarypackagenameID, BPPH._channel),
635 having=(Count() > 1))
592 main_clauses = bpph_location_clauses + [636 main_clauses = bpph_location_clauses + [
593 BPR.id == BPPH.binarypackagereleaseID,637 BPR.id == BPPH.binarypackagereleaseID,
594 BPR.binarypackagenameID.is_in(candidate_binary_names),638 BPR.binarypackagenameID.is_in(candidate_binary_names),
@@ -664,13 +708,14 @@ class Dominator:
664 bins = self.findBinariesForDomination(distroarchseries, pocket)708 bins = self.findBinariesForDomination(distroarchseries, pocket)
665 sorted_packages = self._sortPackages(bins, generalization)709 sorted_packages = self._sortPackages(bins, generalization)
666 self.logger.info("Planning domination of binaries...")710 self.logger.info("Planning domination of binaries...")
667 for name, pubs in sorted_packages.items():711 for (name, location), pubs in sorted_packages.items():
668 self.logger.debug("Planning domination of %s" % name)712 self.logger.debug(
713 "Planning domination of %s in %s" % (name, location))
669 assert len(pubs) > 0, "Dominating zero binaries!"714 assert len(pubs) > 0, "Dominating zero binaries!"
670 live_versions = find_live_binary_versions_pass_1(pubs)715 live_versions = find_live_binary_versions_pass_1(pubs)
671 plan(pubs, live_versions)716 plan(pubs, live_versions)
672 if contains_arch_indep(pubs):717 if contains_arch_indep(pubs):
673 packages_w_arch_indep.add(name)718 packages_w_arch_indep.add((name, location))
674719
675 execute_plan()720 execute_plan()
676721
@@ -692,9 +737,11 @@ class Dominator:
692 bins = self.findBinariesForDomination(distroarchseries, pocket)737 bins = self.findBinariesForDomination(distroarchseries, pocket)
693 sorted_packages = self._sortPackages(bins, generalization)738 sorted_packages = self._sortPackages(bins, generalization)
694 self.logger.info("Planning domination of binaries...(2nd pass)")739 self.logger.info("Planning domination of binaries...(2nd pass)")
695 for name in packages_w_arch_indep.intersection(sorted_packages):740 for name, location in packages_w_arch_indep.intersection(
696 pubs = sorted_packages[name]741 sorted_packages):
697 self.logger.debug("Planning domination of %s" % name)742 pubs = sorted_packages[(name, location)]
743 self.logger.debug(
744 "Planning domination of %s in %s" % (name, location))
698 assert len(pubs) > 0, "Dominating zero binaries in 2nd pass!"745 assert len(pubs) > 0, "Dominating zero binaries in 2nd pass!"
699 live_versions = find_live_binary_versions_pass_2(746 live_versions = find_live_binary_versions_pass_2(
700 pubs, reprieve_cache)747 pubs, reprieve_cache)
@@ -732,7 +779,7 @@ class Dominator:
732 candidate_source_names = Select(779 candidate_source_names = Select(
733 SPPH.sourcepackagenameID,780 SPPH.sourcepackagenameID,
734 And(join_spph_spr(), spph_location_clauses),781 And(join_spph_spr(), spph_location_clauses),
735 group_by=SPPH.sourcepackagenameID,782 group_by=(SPPH.sourcepackagenameID, SPPH._channel),
736 having=(Count() > 1))783 having=(Count() > 1))
737784
738 # We'll also access the SourcePackageReleases associated with785 # We'll also access the SourcePackageReleases associated with
@@ -769,8 +816,8 @@ class Dominator:
769 delete = []816 delete = []
770817
771 self.logger.debug("Dominating sources...")818 self.logger.debug("Dominating sources...")
772 for name, pubs in sorted_packages.items():819 for (name, location), pubs in sorted_packages.items():
773 self.logger.debug("Dominating %s" % name)820 self.logger.debug("Dominating %s in %s" % (name, location))
774 assert len(pubs) > 0, "Dominating zero sources!"821 assert len(pubs) > 0, "Dominating zero sources!"
775 live_versions = find_live_source_versions(pubs)822 live_versions = find_live_source_versions(pubs)
776 cur_supersede, _, cur_delete = self.planPackageDomination(823 cur_supersede, _, cur_delete = self.planPackageDomination(
diff --git a/lib/lp/archivepublisher/publishing.py b/lib/lp/archivepublisher/publishing.py
index 0639716..69fb7e3 100644
--- a/lib/lp/archivepublisher/publishing.py
+++ b/lib/lp/archivepublisher/publishing.py
@@ -680,6 +680,9 @@ class Publisher:
680 judgejudy = Dominator(self.log, self.archive)680 judgejudy = Dominator(self.log, self.archive)
681 for distroseries in self.distro.series:681 for distroseries in self.distro.series:
682 for pocket in self.archive.getPockets():682 for pocket in self.archive.getPockets():
683 # XXX cjwatson 2022-05-19: Channels are handled in the
684 # dominator instead; see the comment in
685 # Dominator._sortPackages.
683 if not self.isAllowed(distroseries, pocket):686 if not self.isAllowed(distroseries, pocket):
684 continue687 continue
685 if not force_domination:688 if not force_domination:
diff --git a/lib/lp/archivepublisher/tests/test_dominator.py b/lib/lp/archivepublisher/tests/test_dominator.py
686old mode 100755689old mode 100755
687new mode 100644690new mode 100644
index 01c56e4..23d5468
--- a/lib/lp/archivepublisher/tests/test_dominator.py
+++ b/lib/lp/archivepublisher/tests/test_dominator.py
@@ -30,7 +30,11 @@ from lp.archivepublisher.publishing import Publisher
30from lp.registry.interfaces.pocket import PackagePublishingPocket30from lp.registry.interfaces.pocket import PackagePublishingPocket
31from lp.registry.interfaces.series import SeriesStatus31from lp.registry.interfaces.series import SeriesStatus
32from lp.services.log.logger import DevNullLogger32from lp.services.log.logger import DevNullLogger
33from lp.soyuz.enums import PackagePublishingStatus33from lp.soyuz.adapters.packagelocation import PackageLocation
34from lp.soyuz.enums import (
35 BinaryPackageFormat,
36 PackagePublishingStatus,
37 )
34from lp.soyuz.interfaces.publishing import (38from lp.soyuz.interfaces.publishing import (
35 IPublishingSet,39 IPublishingSet,
36 ISourcePackagePublishingHistory,40 ISourcePackagePublishingHistory,
@@ -168,29 +172,39 @@ class TestDominator(TestNativePublishingBase):
168 """Domination asserts for non-empty input list."""172 """Domination asserts for non-empty input list."""
169 with lp_dbuser():173 with lp_dbuser():
170 distroseries = self.factory.makeDistroArchSeries().distroseries174 distroseries = self.factory.makeDistroArchSeries().distroseries
175 pocket = self.factory.getAnyPocket()
171 package = self.factory.makeBinaryPackageName()176 package = self.factory.makeBinaryPackageName()
177 location = PackageLocation(
178 archive=self.ubuntutest.main_archive,
179 distribution=distroseries.distribution, distroseries=distroseries,
180 pocket=pocket)
172 dominator = Dominator(self.logger, self.ubuntutest.main_archive)181 dominator = Dominator(self.logger, self.ubuntutest.main_archive)
173 dominator._sortPackages = FakeMethod({package.name: []})182 dominator._sortPackages = FakeMethod({(package.name, location): []})
174 # This isn't a really good exception. It should probably be183 # This isn't a really good exception. It should probably be
175 # something more indicative of bad input.184 # something more indicative of bad input.
176 self.assertRaises(185 self.assertRaises(
177 AssertionError,186 AssertionError,
178 dominator.dominateBinaries,187 dominator.dominateBinaries,
179 distroseries, self.factory.getAnyPocket())188 distroseries, pocket)
180189
181 def test_dominateSources_rejects_empty_publication_list(self):190 def test_dominateSources_rejects_empty_publication_list(self):
182 """Domination asserts for non-empty input list."""191 """Domination asserts for non-empty input list."""
183 with lp_dbuser():192 with lp_dbuser():
184 distroseries = self.factory.makeDistroSeries()193 distroseries = self.factory.makeDistroSeries()
194 pocket = self.factory.getAnyPocket()
185 package = self.factory.makeSourcePackageName()195 package = self.factory.makeSourcePackageName()
196 location = PackageLocation(
197 archive=self.ubuntutest.main_archive,
198 distribution=distroseries.distribution, distroseries=distroseries,
199 pocket=pocket)
186 dominator = Dominator(self.logger, self.ubuntutest.main_archive)200 dominator = Dominator(self.logger, self.ubuntutest.main_archive)
187 dominator._sortPackages = FakeMethod({package.name: []})201 dominator._sortPackages = FakeMethod({(package.name, location): []})
188 # This isn't a really good exception. It should probably be202 # This isn't a really good exception. It should probably be
189 # something more indicative of bad input.203 # something more indicative of bad input.
190 self.assertRaises(204 self.assertRaises(
191 AssertionError,205 AssertionError,
192 dominator.dominateSources,206 dominator.dominateSources,
193 distroseries, self.factory.getAnyPocket())207 distroseries, pocket)
194208
195 def test_archall_domination(self):209 def test_archall_domination(self):
196 # Arch-all binaries should not be dominated when a new source210 # Arch-all binaries should not be dominated when a new source
@@ -398,6 +412,51 @@ class TestDominator(TestNativePublishingBase):
398 for pub in overrides_2:412 for pub in overrides_2:
399 self.assertEqual(PackagePublishingStatus.PUBLISHED, pub.status)413 self.assertEqual(PackagePublishingStatus.PUBLISHED, pub.status)
400414
415 def test_dominate_by_channel(self):
416 # Publications only dominate other publications in the same channel.
417 # (Currently only tested for binary publications.)
418 with lp_dbuser():
419 archive = self.factory.makeArchive()
420 distroseries = self.factory.makeDistroSeries(
421 distribution=archive.distribution)
422 das = self.factory.makeDistroArchSeries(distroseries=distroseries)
423 repository = self.factory.makeGitRepository(
424 target=self.factory.makeDistributionSourcePackage(
425 distribution=archive.distribution))
426 ci_builds = [
427 self.factory.makeCIBuild(
428 git_repository=repository, distro_arch_series=das)
429 for _ in range(3)]
430 bpn = self.factory.makeBinaryPackageName()
431 bprs = [
432 self.factory.makeBinaryPackageRelease(
433 binarypackagename=bpn, version=version, ci_build=ci_build,
434 binpackageformat=BinaryPackageFormat.WHL)
435 for version, ci_build in zip(("1.0", "1.1", "1.2"), ci_builds)]
436 stable_bpphs = [
437 self.factory.makeBinaryPackagePublishingHistory(
438 binarypackagerelease=bpr, archive=archive,
439 distroarchseries=das, status=PackagePublishingStatus.PUBLISHED,
440 pocket=PackagePublishingPocket.RELEASE, channel="stable")
441 for bpr in bprs[:2]]
442 candidate_bpph = self.factory.makeBinaryPackagePublishingHistory(
443 binarypackagerelease=bprs[2], archive=archive,
444 distroarchseries=das, status=PackagePublishingStatus.PUBLISHED,
445 pocket=PackagePublishingPocket.RELEASE, channel="candidate")
446
447 dominator = Dominator(self.logger, archive)
448 dominator.judgeAndDominate(
449 distroseries, PackagePublishingPocket.RELEASE)
450
451 # The older of the two stable publications is superseded, while the
452 # current stable publication and the candidate publication are left
453 # alone.
454 self.checkPublication(
455 stable_bpphs[0], PackagePublishingStatus.SUPERSEDED)
456 self.checkPublications(
457 (stable_bpphs[1], candidate_bpph),
458 PackagePublishingStatus.PUBLISHED)
459
401460
402class TestDomination(TestNativePublishingBase):461class TestDomination(TestNativePublishingBase):
403 """Test overall domination procedure."""462 """Test overall domination procedure."""
@@ -1315,7 +1374,7 @@ class TestArchSpecificPublicationsCache(TestCaseWithFactory):
1315 return removeSecurityProxy(self.factory.makeSourcePackageRelease())1374 return removeSecurityProxy(self.factory.makeSourcePackageRelease())
13161375
1317 def makeBPPH(self, spr=None, arch_specific=True, archive=None,1376 def makeBPPH(self, spr=None, arch_specific=True, archive=None,
1318 distroseries=None):1377 distroseries=None, binpackageformat=None, channel=None):
1319 """Create a `BinaryPackagePublishingHistory`."""1378 """Create a `BinaryPackagePublishingHistory`."""
1320 if spr is None:1379 if spr is None:
1321 spr = self.makeSPR()1380 spr = self.makeSPR()
@@ -1323,12 +1382,13 @@ class TestArchSpecificPublicationsCache(TestCaseWithFactory):
1323 bpb = self.factory.makeBinaryPackageBuild(1382 bpb = self.factory.makeBinaryPackageBuild(
1324 source_package_release=spr, distroarchseries=das)1383 source_package_release=spr, distroarchseries=das)
1325 bpr = self.factory.makeBinaryPackageRelease(1384 bpr = self.factory.makeBinaryPackageRelease(
1326 build=bpb, architecturespecific=arch_specific)1385 build=bpb, binpackageformat=binpackageformat,
1386 architecturespecific=arch_specific)
1327 return removeSecurityProxy(1387 return removeSecurityProxy(
1328 self.factory.makeBinaryPackagePublishingHistory(1388 self.factory.makeBinaryPackagePublishingHistory(
1329 binarypackagerelease=bpr, archive=archive,1389 binarypackagerelease=bpr, archive=archive,
1330 distroarchseries=das, pocket=PackagePublishingPocket.UPDATES,1390 distroarchseries=das, pocket=PackagePublishingPocket.UPDATES,
1331 status=PackagePublishingStatus.PUBLISHED))1391 status=PackagePublishingStatus.PUBLISHED, channel=channel))
13321392
1333 def test_getKey_is_consistent_and_distinguishing(self):1393 def test_getKey_is_consistent_and_distinguishing(self):
1334 # getKey consistently returns the same key for the same BPPH,1394 # getKey consistently returns the same key for the same BPPH,
@@ -1351,14 +1411,19 @@ class TestArchSpecificPublicationsCache(TestCaseWithFactory):
1351 spr, arch_specific=False, archive=dependent.archive,1411 spr, arch_specific=False, archive=dependent.archive,
1352 distroseries=dependent.distroseries)1412 distroseries=dependent.distroseries)
1353 bpph2 = self.makeBPPH(arch_specific=False)1413 bpph2 = self.makeBPPH(arch_specific=False)
1414 bpph3 = self.makeBPPH(
1415 arch_specific=False, binpackageformat=BinaryPackageFormat.WHL,
1416 channel="edge")
1354 cache = self.makeCache()1417 cache = self.makeCache()
1355 self.assertEqual(1418 self.assertEqual(
1356 [True, True, False, False],1419 [True, True, False, False, False, False],
1357 [1420 [
1358 cache.hasArchSpecificPublications(bpph1),1421 cache.hasArchSpecificPublications(bpph1),
1359 cache.hasArchSpecificPublications(bpph1),1422 cache.hasArchSpecificPublications(bpph1),
1360 cache.hasArchSpecificPublications(bpph2),1423 cache.hasArchSpecificPublications(bpph2),
1361 cache.hasArchSpecificPublications(bpph2),1424 cache.hasArchSpecificPublications(bpph2),
1425 cache.hasArchSpecificPublications(bpph3),
1426 cache.hasArchSpecificPublications(bpph3),
1362 ])1427 ])
13631428
1364 def test_hasArchSpecificPublications_caches_results(self):1429 def test_hasArchSpecificPublications_caches_results(self):
diff --git a/lib/lp/code/interfaces/cibuild.py b/lib/lp/code/interfaces/cibuild.py
index 6146dfe..fc7a197 100644
--- a/lib/lp/code/interfaces/cibuild.py
+++ b/lib/lp/code/interfaces/cibuild.py
@@ -177,6 +177,14 @@ class ICIBuildView(IPackageBuildView, IPrivacy):
177 :return: A collection of URLs for this build.177 :return: A collection of URLs for this build.
178 """178 """
179179
180 def createBinaryPackageRelease(
181 binarypackagename, version, summary, description, binpackageformat,
182 architecturespecific, installedsize=None, homepage=None):
183 """Create and return a `BinaryPackageRelease` for this CI build.
184
185 The new binary package release will be linked to this build.
186 """
187
180188
181class ICIBuildEdit(IBuildFarmJobEdit):189class ICIBuildEdit(IBuildFarmJobEdit):
182 """`ICIBuild` methods that require launchpad.Edit."""190 """`ICIBuild` methods that require launchpad.Edit."""
diff --git a/lib/lp/code/model/cibuild.py b/lib/lp/code/model/cibuild.py
index d789a95..4e1ddbd 100644
--- a/lib/lp/code/model/cibuild.py
+++ b/lib/lp/code/model/cibuild.py
@@ -85,6 +85,7 @@ from lp.services.macaroons.interfaces import (
85 )85 )
86from lp.services.macaroons.model import MacaroonIssuerBase86from lp.services.macaroons.model import MacaroonIssuerBase
87from lp.services.propertycache import cachedproperty87from lp.services.propertycache import cachedproperty
88from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
88from lp.soyuz.model.distroarchseries import DistroArchSeries89from lp.soyuz.model.distroarchseries import DistroArchSeries
8990
9091
@@ -465,6 +466,18 @@ class CIBuild(PackageBuildMixin, StormBase):
465 """See `IPackageBuild`."""466 """See `IPackageBuild`."""
466 # We don't currently send any notifications.467 # We don't currently send any notifications.
467468
469 def createBinaryPackageRelease(
470 self, binarypackagename, version, summary, description,
471 binpackageformat, architecturespecific, installedsize=None,
472 homepage=None):
473 """See `ICIBuild`."""
474 return BinaryPackageRelease(
475 ci_build=self, binarypackagename=binarypackagename,
476 version=version, summary=summary, description=description,
477 binpackageformat=binpackageformat,
478 architecturespecific=architecturespecific,
479 installedsize=installedsize, homepage=homepage)
480
468481
469@implementer(ICIBuildSet)482@implementer(ICIBuildSet)
470class CIBuildSet(SpecificBuildFarmJobSourceMixin):483class CIBuildSet(SpecificBuildFarmJobSourceMixin):
diff --git a/lib/lp/code/model/tests/test_cibuild.py b/lib/lp/code/model/tests/test_cibuild.py
index 899b09b..b7e0ca5 100644
--- a/lib/lp/code/model/tests/test_cibuild.py
+++ b/lib/lp/code/model/tests/test_cibuild.py
@@ -70,6 +70,7 @@ from lp.services.macaroons.interfaces import IMacaroonIssuer
70from lp.services.macaroons.testing import MacaroonTestMixin70from lp.services.macaroons.testing import MacaroonTestMixin
71from lp.services.propertycache import clear_property_cache71from lp.services.propertycache import clear_property_cache
72from lp.services.webapp.interfaces import OAuthPermission72from lp.services.webapp.interfaces import OAuthPermission
73from lp.soyuz.enums import BinaryPackageFormat
73from lp.testing import (74from lp.testing import (
74 ANONYMOUS,75 ANONYMOUS,
75 api_url,76 api_url,
@@ -399,6 +400,24 @@ class TestCIBuild(TestCaseWithFactory):
399 commit_sha1=build.commit_sha1,400 commit_sha1=build.commit_sha1,
400 ci_build=build))401 ci_build=build))
401402
403 def test_createBinaryPackageRelease(self):
404 build = self.factory.makeCIBuild()
405 bpn = self.factory.makeBinaryPackageName()
406 bpr = build.createBinaryPackageRelease(
407 bpn, "1.0", "test summary", "test description",
408 BinaryPackageFormat.WHL, False, installedsize=1024,
409 homepage="https://example.com/")
410 self.assertThat(bpr, MatchesStructure(
411 binarypackagename=Equals(bpn),
412 version=Equals("1.0"),
413 summary=Equals("test summary"),
414 description=Equals("test description"),
415 binpackageformat=Equals(BinaryPackageFormat.WHL),
416 architecturespecific=Is(False),
417 installedsize=Equals(1024),
418 homepage=Equals("https://example.com/"),
419 ))
420
402421
403class TestCIBuildSet(TestCaseWithFactory):422class TestCIBuildSet(TestCaseWithFactory):
404423
diff --git a/lib/lp/soyuz/adapters/packagelocation.py b/lib/lp/soyuz/adapters/packagelocation.py
index 6644f1e..2cf1cb4 100644
--- a/lib/lp/soyuz/adapters/packagelocation.py
+++ b/lib/lp/soyuz/adapters/packagelocation.py
@@ -29,9 +29,10 @@ class PackageLocation:
29 pocket = None29 pocket = None
30 component = None30 component = None
31 packagesets = None31 packagesets = None
32 channel = None
3233
33 def __init__(self, archive, distribution, distroseries, pocket,34 def __init__(self, archive, distribution, distroseries, pocket,
34 component=None, packagesets=None):35 component=None, packagesets=None, channel=None):
35 """Initialize the PackageLocation from the given parameters."""36 """Initialize the PackageLocation from the given parameters."""
36 self.archive = archive37 self.archive = archive
37 self.distribution = distribution38 self.distribution = distribution
@@ -39,6 +40,7 @@ class PackageLocation:
39 self.pocket = pocket40 self.pocket = pocket
40 self.component = component41 self.component = component
41 self.packagesets = packagesets or []42 self.packagesets = packagesets or []
43 self.channel = channel
4244
43 def __eq__(self, other):45 def __eq__(self, other):
44 if (self.distribution == other.distribution and46 if (self.distribution == other.distribution and
@@ -46,10 +48,22 @@ class PackageLocation:
46 self.distroseries == other.distroseries and48 self.distroseries == other.distroseries and
47 self.component == other.component and49 self.component == other.component and
48 self.pocket == other.pocket and50 self.pocket == other.pocket and
49 self.packagesets == other.packagesets):51 self.packagesets == other.packagesets and
52 self.channel == other.channel):
50 return True53 return True
51 return False54 return False
5255
56 def __hash__(self):
57 return hash((
58 self.archive,
59 self.distribution,
60 self.distroseries,
61 self.pocket,
62 self.component,
63 None if self.packagesets is None else tuple(self.packagesets),
64 self.channel,
65 ))
66
53 def __str__(self):67 def __str__(self):
54 result = '%s: %s-%s' % (68 result = '%s: %s-%s' % (
55 self.archive.reference, self.distroseries.name, self.pocket.name)69 self.archive.reference, self.distroseries.name, self.pocket.name)
@@ -61,6 +75,9 @@ class PackageLocation:
61 result += ' [%s]' % (75 result += ' [%s]' % (
62 ", ".join([str(p.name) for p in self.packagesets]),)76 ", ".join([str(p.name) for p in self.packagesets]),)
6377
78 if self.channel is not None:
79 result += ' {%s}' % self.channel
80
64 return result81 return result
6582
6683
@@ -70,7 +87,7 @@ class PackageLocationError(Exception):
7087
71def build_package_location(distribution_name, suite=None, purpose=None,88def build_package_location(distribution_name, suite=None, purpose=None,
72 person_name=None, archive_name=None,89 person_name=None, archive_name=None,
73 packageset_names=None):90 packageset_names=None, channel=None):
74 """Convenience function to build PackageLocation objects."""91 """Convenience function to build PackageLocation objects."""
7592
76 # XXX kiko 2007-10-24:93 # XXX kiko 2007-10-24:
@@ -143,6 +160,10 @@ def build_package_location(distribution_name, suite=None, purpose=None,
143 distroseries = distribution.currentseries160 distroseries = distribution.currentseries
144 pocket = PackagePublishingPocket.RELEASE161 pocket = PackagePublishingPocket.RELEASE
145162
163 if pocket != PackagePublishingPocket.RELEASE and channel is not None:
164 raise PackageLocationError(
165 "Channels may only be used with the RELEASE pocket.")
166
146 packagesets = []167 packagesets = []
147 if packageset_names:168 if packageset_names:
148 packageset_set = getUtility(IPackagesetSet)169 packageset_set = getUtility(IPackagesetSet)
@@ -155,5 +176,6 @@ def build_package_location(distribution_name, suite=None, purpose=None,
155 "Could not find packageset %s" % err)176 "Could not find packageset %s" % err)
156 packagesets.append(packageset)177 packagesets.append(packageset)
157178
158 return PackageLocation(archive, distribution, distroseries, pocket,179 return PackageLocation(
159 packagesets=packagesets)180 archive, distribution, distroseries, pocket,
181 packagesets=packagesets, channel=channel)
diff --git a/lib/lp/soyuz/adapters/tests/test_packagelocation.py b/lib/lp/soyuz/adapters/tests/test_packagelocation.py
index 3d45f14..9bbef24 100644
--- a/lib/lp/soyuz/adapters/tests/test_packagelocation.py
+++ b/lib/lp/soyuz/adapters/tests/test_packagelocation.py
@@ -22,11 +22,12 @@ class TestPackageLocation(TestCaseWithFactory):
2222
23 def getPackageLocation(self, distribution_name='ubuntu', suite=None,23 def getPackageLocation(self, distribution_name='ubuntu', suite=None,
24 purpose=None, person_name=None,24 purpose=None, person_name=None,
25 archive_name=None, packageset_names=None):25 archive_name=None, packageset_names=None,
26 channel=None):
26 """Use a helper method to setup a `PackageLocation` object."""27 """Use a helper method to setup a `PackageLocation` object."""
27 return build_package_location(28 return build_package_location(
28 distribution_name, suite, purpose, person_name, archive_name,29 distribution_name, suite, purpose, person_name, archive_name,
29 packageset_names=packageset_names)30 packageset_names=packageset_names, channel=channel)
3031
31 def testSetupLocationForCOPY(self):32 def testSetupLocationForCOPY(self):
32 """`PackageLocation` for COPY archives."""33 """`PackageLocation` for COPY archives."""
@@ -150,6 +151,15 @@ class TestPackageLocation(TestCaseWithFactory):
150 distribution_name='debian',151 distribution_name='debian',
151 packageset_names=[packageset_name, "unknown"])152 packageset_names=[packageset_name, "unknown"])
152153
154 def test_build_package_location_with_channel_outside_release_pocket(self):
155 """It doesn't make sense to use non-RELEASE pockets with channels."""
156 self.assertRaisesWithContent(
157 PackageLocationError,
158 "Channels may only be used with the RELEASE pocket.",
159 self.getPackageLocation,
160 suite="warty-security",
161 channel="stable")
162
153 def testSetupLocationPPANotMatchingDistribution(self):163 def testSetupLocationPPANotMatchingDistribution(self):
154 """`PackageLocationError` is raised when PPA does not match the164 """`PackageLocationError` is raised when PPA does not match the
155 distribution."""165 distribution."""
@@ -214,6 +224,14 @@ class TestPackageLocation(TestCaseWithFactory):
214 self.assertEqual(224 self.assertEqual(
215 location_ubuntu_hoary, location_ubuntu_hoary_again)225 location_ubuntu_hoary, location_ubuntu_hoary_again)
216226
227 def testCompareChannels(self):
228 location_ubuntu_hoary = self.getPackageLocation(channel="stable")
229 location_ubuntu_hoary_again = self.getPackageLocation(channel="edge")
230 self.assertNotEqual(location_ubuntu_hoary, location_ubuntu_hoary_again)
231
232 location_ubuntu_hoary_again.channel = "stable"
233 self.assertEqual(location_ubuntu_hoary, location_ubuntu_hoary_again)
234
217 def testRepresentation(self):235 def testRepresentation(self):
218 """Check if PackageLocation is represented correctly."""236 """Check if PackageLocation is represented correctly."""
219 location_ubuntu_hoary = self.getPackageLocation()237 location_ubuntu_hoary = self.getPackageLocation()
diff --git a/lib/lp/soyuz/enums.py b/lib/lp/soyuz/enums.py
index d60ea48..9953f43 100644
--- a/lib/lp/soyuz/enums.py
+++ b/lib/lp/soyuz/enums.py
@@ -208,6 +208,13 @@ class BinaryPackageFileType(DBEnumeratedType):
208 build environment.208 build environment.
209 """)209 """)
210210
211 WHL = DBItem(6, """
212 Python Wheel
213
214 The "wheel" binary package format for Python, originally defined in
215 U{https://peps.python.org/pep-0427/}.
216 """)
217
211218
212class BinaryPackageFormat(DBEnumeratedType):219class BinaryPackageFormat(DBEnumeratedType):
213 """Binary Package Format220 """Binary Package Format
@@ -251,6 +258,13 @@ class BinaryPackageFormat(DBEnumeratedType):
251 This is the binary package format used for shipping debug symbols258 This is the binary package format used for shipping debug symbols
252 in Ubuntu and similar distributions.""")259 in Ubuntu and similar distributions.""")
253260
261 WHL = DBItem(6, """
262 Python Wheel
263
264 The "wheel" binary package format for Python, originally defined in
265 U{https://peps.python.org/pep-0427/}.
266 """)
267
254268
255class PackageCopyPolicy(DBEnumeratedType):269class PackageCopyPolicy(DBEnumeratedType):
256 """Package copying policy.270 """Package copying policy.
diff --git a/lib/lp/soyuz/interfaces/publishing.py b/lib/lp/soyuz/interfaces/publishing.py
index a1cfeab..21a3cac 100644
--- a/lib/lp/soyuz/interfaces/publishing.py
+++ b/lib/lp/soyuz/interfaces/publishing.py
@@ -50,7 +50,6 @@ from zope.schema import (
50 Date,50 Date,
51 Datetime,51 Datetime,
52 Int,52 Int,
53 List,
54 Text,53 Text,
55 TextLine,54 TextLine,
56 )55 )
@@ -269,9 +268,8 @@ class ISourcePackagePublishingHistoryPublic(IPublishingView):
269 vocabulary=PackagePublishingPocket,268 vocabulary=PackagePublishingPocket,
270 required=True, readonly=True,269 required=True, readonly=True,
271 ))270 ))
272 channel = List(271 channel = TextLine(
273 value_type=TextLine(), title=_("Channel"),272 title=_("Channel"), required=False, readonly=False,
274 required=False, readonly=False,
275 description=_(273 description=_(
276 "The channel into which this entry is published "274 "The channel into which this entry is published "
277 "(only for archives published using Artifactory)"))275 "(only for archives published using Artifactory)"))
@@ -700,9 +698,8 @@ class IBinaryPackagePublishingHistoryPublic(IPublishingView):
700 vocabulary=PackagePublishingPocket,698 vocabulary=PackagePublishingPocket,
701 required=True, readonly=True,699 required=True, readonly=True,
702 ))700 ))
703 channel = List(701 channel = TextLine(
704 value_type=TextLine(), title=_("Channel"),702 title=_("Channel"), required=False, readonly=False,
705 required=False, readonly=False,
706 description=_(703 description=_(
707 "The channel into which this entry is published "704 "The channel into which this entry is published "
708 "(only for archives published using Artifactory)"))705 "(only for archives published using Artifactory)"))
@@ -970,7 +967,8 @@ class IPublishingSet(Interface):
970 def newSourcePublication(archive, sourcepackagerelease, distroseries,967 def newSourcePublication(archive, sourcepackagerelease, distroseries,
971 component, section, pocket, ancestor,968 component, section, pocket, ancestor,
972 create_dsd_job=True, copied_from_archive=None,969 create_dsd_job=True, copied_from_archive=None,
973 creator=None, sponsor=None, packageupload=None):970 creator=None, sponsor=None, packageupload=None,
971 channel=None):
974 """Create a new `SourcePackagePublishingHistory`.972 """Create a new `SourcePackagePublishingHistory`.
975973
976 :param archive: An `IArchive`974 :param archive: An `IArchive`
diff --git a/lib/lp/soyuz/model/binarypackagerelease.py b/lib/lp/soyuz/model/binarypackagerelease.py
index 25cae48..7134895 100644
--- a/lib/lp/soyuz/model/binarypackagerelease.py
+++ b/lib/lp/soyuz/model/binarypackagerelease.py
@@ -157,6 +157,8 @@ class BinaryPackageRelease(SQLBase):
157 determined_filetype = BinaryPackageFileType.UDEB157 determined_filetype = BinaryPackageFileType.UDEB
158 elif file.filename.endswith(".ddeb"):158 elif file.filename.endswith(".ddeb"):
159 determined_filetype = BinaryPackageFileType.DDEB159 determined_filetype = BinaryPackageFileType.DDEB
160 elif file.filename.endswith(".whl"):
161 determined_filetype = BinaryPackageFileType.WHL
160 else:162 else:
161 raise AssertionError(163 raise AssertionError(
162 'Unsupported file type: %s' % file.filename)164 'Unsupported file type: %s' % file.filename)
diff --git a/lib/lp/soyuz/model/publishing.py b/lib/lp/soyuz/model/publishing.py
index cdab414..8a397b6 100644
--- a/lib/lp/soyuz/model/publishing.py
+++ b/lib/lp/soyuz/model/publishing.py
@@ -12,6 +12,7 @@ __all__ = [
1212
13from collections import defaultdict13from collections import defaultdict
14from datetime import datetime14from datetime import datetime
15import json
15from operator import (16from operator import (
16 attrgetter,17 attrgetter,
17 itemgetter,18 itemgetter,
@@ -20,6 +21,7 @@ from pathlib import Path
20import sys21import sys
2122
22import pytz23import pytz
24from storm.databases.postgres import JSON
23from storm.expr import (25from storm.expr import (
24 And,26 And,
25 Cast,27 Cast,
@@ -31,10 +33,6 @@ from storm.expr import (
31 Sum,33 Sum,
32 )34 )
33from storm.info import ClassAlias35from storm.info import ClassAlias
34from storm.properties import (
35 List,
36 Unicode,
37 )
38from storm.store import Store36from storm.store import Store
39from storm.zope import IResultSet37from storm.zope import IResultSet
40from storm.zope.interfaces import ISQLObjectResultSet38from storm.zope.interfaces import ISQLObjectResultSet
@@ -51,6 +49,10 @@ from lp.registry.interfaces.person import validate_public_person
51from lp.registry.interfaces.pocket import PackagePublishingPocket49from lp.registry.interfaces.pocket import PackagePublishingPocket
52from lp.registry.interfaces.sourcepackage import SourcePackageType50from lp.registry.interfaces.sourcepackage import SourcePackageType
53from lp.registry.model.sourcepackagename import SourcePackageName51from lp.registry.model.sourcepackagename import SourcePackageName
52from lp.services.channels import (
53 channel_list_to_string,
54 channel_string_to_list,
55 )
54from lp.services.database import bulk56from lp.services.database import bulk
55from lp.services.database.constants import UTC_NOW57from lp.services.database.constants import UTC_NOW
56from lp.services.database.datetimecol import UtcDateTimeCol58from lp.services.database.datetimecol import UtcDateTimeCol
@@ -267,7 +269,7 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
267 pocket = DBEnum(name='pocket', enum=PackagePublishingPocket,269 pocket = DBEnum(name='pocket', enum=PackagePublishingPocket,
268 default=PackagePublishingPocket.RELEASE,270 default=PackagePublishingPocket.RELEASE,
269 allow_none=False)271 allow_none=False)
270 channel = List(name="channel", type=Unicode(), allow_none=True)272 _channel = JSON(name="channel", allow_none=True)
271 archive = ForeignKey(dbName="archive", foreignKey="Archive", notNull=True)273 archive = ForeignKey(dbName="archive", foreignKey="Archive", notNull=True)
272 copied_from_archive = ForeignKey(274 copied_from_archive = ForeignKey(
273 dbName="copied_from_archive", foreignKey="Archive", notNull=False)275 dbName="copied_from_archive", foreignKey="Archive", notNull=False)
@@ -315,6 +317,13 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
315 self.distroseries.setNewerDistroSeriesVersions([self])317 self.distroseries.setNewerDistroSeriesVersions([self])
316 return get_property_cache(self).newer_distroseries_version318 return get_property_cache(self).newer_distroseries_version
317319
320 @property
321 def channel(self):
322 """See `ISourcePackagePublishingHistory`."""
323 if self._channel is None:
324 return None
325 return channel_list_to_string(*self._channel)
326
318 def getPublishedBinaries(self):327 def getPublishedBinaries(self):
319 """See `ISourcePackagePublishingHistory`."""328 """See `ISourcePackagePublishingHistory`."""
320 publishing_set = getUtility(IPublishingSet)329 publishing_set = getUtility(IPublishingSet)
@@ -538,7 +547,8 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
538 component=new_component,547 component=new_component,
539 section=new_section,548 section=new_section,
540 creator=creator,549 creator=creator,
541 archive=self.archive)550 archive=self.archive,
551 channel=self.channel)
542552
543 def copyTo(self, distroseries, pocket, archive, override=None,553 def copyTo(self, distroseries, pocket, archive, override=None,
544 create_dsd_job=True, creator=None, sponsor=None,554 create_dsd_job=True, creator=None, sponsor=None,
@@ -564,7 +574,8 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
564 creator=creator,574 creator=creator,
565 sponsor=sponsor,575 sponsor=sponsor,
566 copied_from_archive=self.archive,576 copied_from_archive=self.archive,
567 packageupload=packageupload)577 packageupload=packageupload,
578 channel=self.channel)
568579
569 def getStatusSummaryForBuilds(self):580 def getStatusSummaryForBuilds(self):
570 """See `ISourcePackagePublishingHistory`."""581 """See `ISourcePackagePublishingHistory`."""
@@ -685,7 +696,7 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
685 datemadepending = UtcDateTimeCol(default=None)696 datemadepending = UtcDateTimeCol(default=None)
686 dateremoved = UtcDateTimeCol(default=None)697 dateremoved = UtcDateTimeCol(default=None)
687 pocket = DBEnum(name='pocket', enum=PackagePublishingPocket)698 pocket = DBEnum(name='pocket', enum=PackagePublishingPocket)
688 channel = List(name="channel", type=Unicode(), allow_none=True)699 _channel = JSON(name="channel", allow_none=True)
689 archive = ForeignKey(dbName="archive", foreignKey="Archive", notNull=True)700 archive = ForeignKey(dbName="archive", foreignKey="Archive", notNull=True)
690 copied_from_archive = ForeignKey(701 copied_from_archive = ForeignKey(
691 dbName="copied_from_archive", foreignKey="Archive", notNull=False)702 dbName="copied_from_archive", foreignKey="Archive", notNull=False)
@@ -779,6 +790,13 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
779 distroseries.name,790 distroseries.name,
780 self.distroarchseries.architecturetag)791 self.distroarchseries.architecturetag)
781792
793 @property
794 def channel(self):
795 """See `ISourcePackagePublishingHistory`."""
796 if self._channel is None:
797 return None
798 return channel_list_to_string(*self._channel)
799
782 def getDownloadCount(self):800 def getDownloadCount(self):
783 """See `IBinaryPackagePublishingHistory`."""801 """See `IBinaryPackagePublishingHistory`."""
784 return self.archive.getPackageDownloadTotal(self.binarypackagerelease)802 return self.archive.getPackageDownloadTotal(self.binarypackagerelease)
@@ -836,21 +854,26 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
836 dominant.distroarchseries.architecturetag))854 dominant.distroarchseries.architecturetag))
837855
838 dominant_build = dominant.binarypackagerelease.build856 dominant_build = dominant.binarypackagerelease.build
839 distroarchseries = dominant_build.distro_arch_series857 # XXX cjwatson 2022-05-01: We can't currently dominate with CI
840 if logger is not None:858 # builds, since supersededby is a reference to a BPB. Just
841 logger.debug(859 # leave supersededby unset in that case for now, which isn't
842 "The %s build of %s has been judged as superseded by the "860 # ideal but will work well enough.
843 "build of %s. Arch-specific == %s" % (861 if dominant_build is not None:
844 distroarchseries.architecturetag,862 distroarchseries = dominant_build.distro_arch_series
845 self.binarypackagerelease.title,863 if logger is not None:
846 dominant_build.source_package_release.title,864 logger.debug(
847 self.binarypackagerelease.architecturespecific))865 "The %s build of %s has been judged as superseded by "
848 # Binary package releases are superseded by the new build,866 "the build of %s. Arch-specific == %s" % (
849 # not the new binary package release. This is because867 distroarchseries.architecturetag,
850 # there may not *be* a new matching binary package -868 self.binarypackagerelease.title,
851 # source packages can change the binaries they build869 dominant_build.source_package_release.title,
852 # between releases.870 self.binarypackagerelease.architecturespecific))
853 self.supersededby = dominant_build871 # Binary package releases are superseded by the new build,
872 # not the new binary package release. This is because
873 # there may not *be* a new matching binary package -
874 # source packages can change the binaries they build
875 # between releases.
876 self.supersededby = dominant_build
854877
855 debug = getUtility(IPublishingSet).findCorrespondingDDEBPublications(878 debug = getUtility(IPublishingSet).findCorrespondingDDEBPublications(
856 [self])879 [self])
@@ -941,7 +964,8 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
941 priority=new_priority,964 priority=new_priority,
942 creator=creator,965 creator=creator,
943 archive=debug.archive,966 archive=debug.archive,
944 phased_update_percentage=new_phased_update_percentage)967 phased_update_percentage=new_phased_update_percentage,
968 _channel=removeSecurityProxy(debug)._channel)
945969
946 # Append the modified package publishing entry970 # Append the modified package publishing entry
947 return BinaryPackagePublishingHistory(971 return BinaryPackagePublishingHistory(
@@ -957,7 +981,8 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
957 priority=new_priority,981 priority=new_priority,
958 archive=self.archive,982 archive=self.archive,
959 creator=creator,983 creator=creator,
960 phased_update_percentage=new_phased_update_percentage)984 phased_update_percentage=new_phased_update_percentage,
985 _channel=self._channel)
961986
962 def copyTo(self, distroseries, pocket, archive):987 def copyTo(self, distroseries, pocket, archive):
963 """See `BinaryPackagePublishingHistory`."""988 """See `BinaryPackagePublishingHistory`."""
@@ -1086,10 +1111,15 @@ class PublishingSet:
1086 """Utilities for manipulating publications in batches."""1111 """Utilities for manipulating publications in batches."""
10871112
1088 def publishBinaries(self, archive, distroseries, pocket, binaries,1113 def publishBinaries(self, archive, distroseries, pocket, binaries,
1089 copied_from_archives=None):1114 copied_from_archives=None, channel=None):
1090 """See `IPublishingSet`."""1115 """See `IPublishingSet`."""
1091 if copied_from_archives is None:1116 if copied_from_archives is None:
1092 copied_from_archives = {}1117 copied_from_archives = {}
1118 if channel is not None:
1119 if pocket != PackagePublishingPocket.RELEASE:
1120 raise AssertionError(
1121 "Channel publications must be in the RELEASE pocket")
1122 channel = channel_string_to_list(channel)
1093 # Expand the dict of binaries into a list of tuples including the1123 # Expand the dict of binaries into a list of tuples including the
1094 # architecture.1124 # architecture.
1095 if distroseries.distribution != archive.distribution:1125 if distroseries.distribution != archive.distribution:
@@ -1124,6 +1154,9 @@ class PublishingSet:
1124 BinaryPackageRelease.binarypackagenameID,1154 BinaryPackageRelease.binarypackagenameID,
1125 BinaryPackageRelease.version),1155 BinaryPackageRelease.version),
1126 BinaryPackagePublishingHistory.pocket == pocket,1156 BinaryPackagePublishingHistory.pocket == pocket,
1157 Not(IsDistinctFrom(
1158 BinaryPackagePublishingHistory._channel,
1159 json.dumps(channel) if channel is not None else None)),
1127 BinaryPackagePublishingHistory.status.is_in(1160 BinaryPackagePublishingHistory.status.is_in(
1128 active_publishing_status),1161 active_publishing_status),
1129 BinaryPackageRelease.id ==1162 BinaryPackageRelease.id ==
@@ -1141,12 +1174,13 @@ class PublishingSet:
1141 BPPH = BinaryPackagePublishingHistory1174 BPPH = BinaryPackagePublishingHistory
1142 return bulk.create(1175 return bulk.create(
1143 (BPPH.archive, BPPH.copied_from_archive,1176 (BPPH.archive, BPPH.copied_from_archive,
1144 BPPH.distroarchseries, BPPH.pocket,1177 BPPH.distroarchseries, BPPH.pocket, BPPH._channel,
1145 BPPH.binarypackagerelease, BPPH.binarypackagename,1178 BPPH.binarypackagerelease, BPPH.binarypackagename,
1179 BPPH._binarypackageformat,
1146 BPPH.component, BPPH.section, BPPH.priority,1180 BPPH.component, BPPH.section, BPPH.priority,
1147 BPPH.phased_update_percentage, BPPH.status, BPPH.datecreated),1181 BPPH.phased_update_percentage, BPPH.status, BPPH.datecreated),
1148 [(archive, copied_from_archives.get(bpr), das, pocket, bpr,1182 [(archive, copied_from_archives.get(bpr), das, pocket, channel,
1149 bpr.binarypackagename,1183 bpr, bpr.binarypackagename, bpr.binpackageformat,
1150 get_component(archive, das.distroseries, component),1184 get_component(archive, das.distroseries, component),
1151 section, priority, phased_update_percentage,1185 section, priority, phased_update_percentage,
1152 PackagePublishingStatus.PENDING, UTC_NOW)1186 PackagePublishingStatus.PENDING, UTC_NOW)
@@ -1156,7 +1190,7 @@ class PublishingSet:
1156 get_objects=True)1190 get_objects=True)
11571191
1158 def copyBinaries(self, archive, distroseries, pocket, bpphs, policy=None,1192 def copyBinaries(self, archive, distroseries, pocket, bpphs, policy=None,
1159 source_override=None):1193 source_override=None, channel=None):
1160 """See `IPublishingSet`."""1194 """See `IPublishingSet`."""
1161 from lp.soyuz.adapters.overrides import BinaryOverride1195 from lp.soyuz.adapters.overrides import BinaryOverride
1162 if distroseries.distribution != archive.distribution:1196 if distroseries.distribution != archive.distribution:
@@ -1228,13 +1262,14 @@ class PublishingSet:
1228 bpph.binarypackagerelease: bpph.archive for bpph in bpphs}1262 bpph.binarypackagerelease: bpph.archive for bpph in bpphs}
1229 return self.publishBinaries(1263 return self.publishBinaries(
1230 archive, distroseries, pocket, with_overrides,1264 archive, distroseries, pocket, with_overrides,
1231 copied_from_archives)1265 copied_from_archives, channel=channel)
12321266
1233 def newSourcePublication(self, archive, sourcepackagerelease,1267 def newSourcePublication(self, archive, sourcepackagerelease,
1234 distroseries, component, section, pocket,1268 distroseries, component, section, pocket,
1235 ancestor=None, create_dsd_job=True,1269 ancestor=None, create_dsd_job=True,
1236 copied_from_archive=None,1270 copied_from_archive=None,
1237 creator=None, sponsor=None, packageupload=None):1271 creator=None, sponsor=None, packageupload=None,
1272 channel=None):
1238 """See `IPublishingSet`."""1273 """See `IPublishingSet`."""
1239 # Circular import.1274 # Circular import.
1240 from lp.registry.model.distributionsourcepackage import (1275 from lp.registry.model.distributionsourcepackage import (
@@ -1246,6 +1281,15 @@ class PublishingSet:
1246 "Series distribution %s doesn't match archive distribution %s."1281 "Series distribution %s doesn't match archive distribution %s."
1247 % (distroseries.distribution.name, archive.distribution.name))1282 % (distroseries.distribution.name, archive.distribution.name))
12481283
1284 if channel is not None:
1285 if sourcepackagerelease.format == SourcePackageType.DPKG:
1286 raise AssertionError(
1287 "Can't publish dpkg source packages to a channel")
1288 if pocket != PackagePublishingPocket.RELEASE:
1289 raise AssertionError(
1290 "Channel publications must be in the RELEASE pocket")
1291 channel = channel_string_to_list(channel)
1292
1249 pub = SourcePackagePublishingHistory(1293 pub = SourcePackagePublishingHistory(
1250 distroseries=distroseries,1294 distroseries=distroseries,
1251 pocket=pocket,1295 pocket=pocket,
@@ -1261,7 +1305,8 @@ class PublishingSet:
1261 ancestor=ancestor,1305 ancestor=ancestor,
1262 creator=creator,1306 creator=creator,
1263 sponsor=sponsor,1307 sponsor=sponsor,
1264 packageupload=packageupload)1308 packageupload=packageupload,
1309 _channel=channel)
1265 DistributionSourcePackage.ensure(pub)1310 DistributionSourcePackage.ensure(pub)
12661311
1267 if create_dsd_job and archive == distroseries.main_archive:1312 if create_dsd_job and archive == distroseries.main_archive:
diff --git a/lib/lp/soyuz/tests/test_publishing.py b/lib/lp/soyuz/tests/test_publishing.py
index 41cdf25..7d91f43 100644
--- a/lib/lp/soyuz/tests/test_publishing.py
+++ b/lib/lp/soyuz/tests/test_publishing.py
@@ -36,6 +36,7 @@ from lp.registry.interfaces.sourcepackage import (
36 SourcePackageUrgency,36 SourcePackageUrgency,
37 )37 )
38from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet38from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
39from lp.services.channels import channel_string_to_list
39from lp.services.config import config40from lp.services.config import config
40from lp.services.database.constants import UTC_NOW41from lp.services.database.constants import UTC_NOW
41from lp.services.librarian.interfaces import ILibraryFileAliasSet42from lp.services.librarian.interfaces import ILibraryFileAliasSet
@@ -212,7 +213,8 @@ class SoyuzTestPublisher:
212 build_conflicts_indep=None,213 build_conflicts_indep=None,
213 dsc_maintainer_rfc822='Foo Bar <foo@bar.com>',214 dsc_maintainer_rfc822='Foo Bar <foo@bar.com>',
214 maintainer=None, creator=None, date_uploaded=UTC_NOW,215 maintainer=None, creator=None, date_uploaded=UTC_NOW,
215 spr_only=False, user_defined_fields=None):216 spr_only=False, user_defined_fields=None,
217 format=SourcePackageType.DPKG, channel=None):
216 """Return a mock source publishing record.218 """Return a mock source publishing record.
217219
218 if spr_only is specified, the source is not published and the220 if spr_only is specified, the source is not published and the
@@ -238,7 +240,7 @@ class SoyuzTestPublisher:
238240
239 spr = distroseries.createUploadedSourcePackageRelease(241 spr = distroseries.createUploadedSourcePackageRelease(
240 sourcepackagename=spn,242 sourcepackagename=spn,
241 format=SourcePackageType.DPKG,243 format=format,
242 maintainer=maintainer,244 maintainer=maintainer,
243 creator=creator,245 creator=creator,
244 component=component,246 component=component,
@@ -289,6 +291,8 @@ class SoyuzTestPublisher:
289 datepublished = UTC_NOW291 datepublished = UTC_NOW
290 else:292 else:
291 datepublished = None293 datepublished = None
294 if channel is not None:
295 channel = channel_string_to_list(channel)
292296
293 spph = SourcePackagePublishingHistory(297 spph = SourcePackagePublishingHistory(
294 distroseries=distroseries,298 distroseries=distroseries,
@@ -304,7 +308,8 @@ class SoyuzTestPublisher:
304 scheduleddeletiondate=scheduleddeletiondate,308 scheduleddeletiondate=scheduleddeletiondate,
305 pocket=pocket,309 pocket=pocket,
306 archive=archive,310 archive=archive,
307 creator=creator)311 creator=creator,
312 _channel=channel)
308313
309 return spph314 return spph
310315
@@ -328,7 +333,8 @@ class SoyuzTestPublisher:
328 builder=None,333 builder=None,
329 component='main',334 component='main',
330 phased_update_percentage=None,335 phased_update_percentage=None,
331 with_debug=False, user_defined_fields=None):336 with_debug=False, user_defined_fields=None,
337 channel=None):
332 """Return a list of binary publishing records."""338 """Return a list of binary publishing records."""
333 if distroseries is None:339 if distroseries is None:
334 distroseries = self.distroseries340 distroseries = self.distroseries
@@ -366,7 +372,7 @@ class SoyuzTestPublisher:
366 pub_binaries += self.publishBinaryInArchive(372 pub_binaries += self.publishBinaryInArchive(
367 binarypackagerelease_ddeb, archive, status,373 binarypackagerelease_ddeb, archive, status,
368 pocket, scheduleddeletiondate, dateremoved,374 pocket, scheduleddeletiondate, dateremoved,
369 phased_update_percentage)375 phased_update_percentage, channel=channel)
370 else:376 else:
371 binarypackagerelease_ddeb = None377 binarypackagerelease_ddeb = None
372378
@@ -378,7 +384,8 @@ class SoyuzTestPublisher:
378 user_defined_fields=user_defined_fields)384 user_defined_fields=user_defined_fields)
379 pub_binaries += self.publishBinaryInArchive(385 pub_binaries += self.publishBinaryInArchive(
380 binarypackagerelease, archive, status, pocket,386 binarypackagerelease, archive, status, pocket,
381 scheduleddeletiondate, dateremoved, phased_update_percentage)387 scheduleddeletiondate, dateremoved, phased_update_percentage,
388 channel=channel)
382 published_binaries.extend(pub_binaries)389 published_binaries.extend(pub_binaries)
383 package_upload = self.addPackageUpload(390 package_upload = self.addPackageUpload(
384 archive, distroseries, pocket,391 archive, distroseries, pocket,
@@ -476,7 +483,7 @@ class SoyuzTestPublisher:
476 status=PackagePublishingStatus.PENDING,483 status=PackagePublishingStatus.PENDING,
477 pocket=PackagePublishingPocket.RELEASE,484 pocket=PackagePublishingPocket.RELEASE,
478 scheduleddeletiondate=None, dateremoved=None,485 scheduleddeletiondate=None, dateremoved=None,
479 phased_update_percentage=None):486 phased_update_percentage=None, channel=None):
480 """Return the corresponding BinaryPackagePublishingHistory."""487 """Return the corresponding BinaryPackagePublishingHistory."""
481 distroarchseries = binarypackagerelease.build.distro_arch_series488 distroarchseries = binarypackagerelease.build.distro_arch_series
482489
@@ -485,6 +492,8 @@ class SoyuzTestPublisher:
485 archs = [distroarchseries]492 archs = [distroarchseries]
486 else:493 else:
487 archs = distroarchseries.distroseries.architectures494 archs = distroarchseries.distroseries.architectures
495 if channel is not None:
496 channel = channel_string_to_list(channel)
488497
489 pub_binaries = []498 pub_binaries = []
490 for arch in archs:499 for arch in archs:
@@ -502,7 +511,8 @@ class SoyuzTestPublisher:
502 datecreated=UTC_NOW,511 datecreated=UTC_NOW,
503 pocket=pocket,512 pocket=pocket,
504 archive=archive,513 archive=archive,
505 phased_update_percentage=phased_update_percentage)514 phased_update_percentage=phased_update_percentage,
515 _channel=channel)
506 if status == PackagePublishingStatus.PUBLISHED:516 if status == PackagePublishingStatus.PUBLISHED:
507 pub.datepublished = UTC_NOW517 pub.datepublished = UTC_NOW
508 pub_binaries.append(pub)518 pub_binaries.append(pub)
@@ -1583,19 +1593,24 @@ class TestPublishBinaries(TestCaseWithFactory):
15831593
1584 layer = LaunchpadZopelessLayer1594 layer = LaunchpadZopelessLayer
15851595
1586 def makeArgs(self, bprs, distroseries, archive=None):1596 def makeArgs(self, bprs, distroseries, archive=None, channel=None):
1587 """Create a dict of arguments for publishBinaries."""1597 """Create a dict of arguments for publishBinaries."""
1588 if archive is None:1598 if archive is None:
1589 archive = distroseries.main_archive1599 archive = distroseries.main_archive
1590 return {1600 args = {
1591 'archive': archive,1601 'archive': archive,
1592 'distroseries': distroseries,1602 'distroseries': distroseries,
1593 'pocket': PackagePublishingPocket.BACKPORTS,1603 'pocket': (
1604 PackagePublishingPocket.BACKPORTS if channel is None
1605 else PackagePublishingPocket.RELEASE),
1594 'binaries': {1606 'binaries': {
1595 bpr: (self.factory.makeComponent(),1607 bpr: (self.factory.makeComponent(),
1596 self.factory.makeSection(),1608 self.factory.makeSection(),
1597 PackagePublishingPriority.REQUIRED, 50) for bpr in bprs},1609 PackagePublishingPriority.REQUIRED, 50) for bpr in bprs},
1598 }1610 }
1611 if channel is not None:
1612 args['channel'] = channel
1613 return args
15991614
1600 def test_architecture_dependent(self):1615 def test_architecture_dependent(self):
1601 # Architecture-dependent binaries get created as PENDING in the1616 # Architecture-dependent binaries get created as PENDING in the
@@ -1614,8 +1629,8 @@ class TestPublishBinaries(TestCaseWithFactory):
1614 overrides = args['binaries'][bpr]1629 overrides = args['binaries'][bpr]
1615 self.assertEqual(bpr, bpph.binarypackagerelease)1630 self.assertEqual(bpr, bpph.binarypackagerelease)
1616 self.assertEqual(1631 self.assertEqual(
1617 (args['archive'], target_das, args['pocket']),1632 (args['archive'], target_das, args['pocket'], None),
1618 (bpph.archive, bpph.distroarchseries, bpph.pocket))1633 (bpph.archive, bpph.distroarchseries, bpph.pocket, bpph.channel))
1619 self.assertEqual(1634 self.assertEqual(
1620 overrides,1635 overrides,
1621 (bpph.component, bpph.section, bpph.priority,1636 (bpph.component, bpph.section, bpph.priority,
@@ -1670,30 +1685,59 @@ class TestPublishBinaries(TestCaseWithFactory):
1670 args['pocket'] = PackagePublishingPocket.RELEASE1685 args['pocket'] = PackagePublishingPocket.RELEASE
1671 [another_bpph] = getUtility(IPublishingSet).publishBinaries(**args)1686 [another_bpph] = getUtility(IPublishingSet).publishBinaries(**args)
16721687
1688 def test_channel(self):
1689 bpr = self.factory.makeBinaryPackageRelease(
1690 binpackageformat=BinaryPackageFormat.WHL)
1691 target_das = self.factory.makeDistroArchSeries()
1692 args = self.makeArgs([bpr], target_das.distroseries, channel="stable")
1693 [bpph] = getUtility(IPublishingSet).publishBinaries(**args)
1694 self.assertEqual(bpr, bpph.binarypackagerelease)
1695 self.assertEqual(
1696 (args["archive"], target_das, args["pocket"], args["channel"]),
1697 (bpph.archive, bpph.distroarchseries, bpph.pocket, bpph.channel))
1698 self.assertEqual(PackagePublishingStatus.PENDING, bpph.status)
1699
1700 def test_does_not_duplicate_by_channel(self):
1701 bpr = self.factory.makeBinaryPackageRelease(
1702 binpackageformat=BinaryPackageFormat.WHL)
1703 target_das = self.factory.makeDistroArchSeries()
1704 args = self.makeArgs([bpr], target_das.distroseries, channel="stable")
1705 [bpph] = getUtility(IPublishingSet).publishBinaries(**args)
1706 self.assertContentEqual(
1707 [], getUtility(IPublishingSet).publishBinaries(**args))
1708 args["channel"] = "edge"
1709 [another_bpph] = getUtility(IPublishingSet).publishBinaries(**args)
1710
16731711
1674class TestChangeOverride(TestNativePublishingBase):1712class TestChangeOverride(TestNativePublishingBase):
1675 """Test that changing overrides works."""1713 """Test that changing overrides works."""
16761714
1677 def setUpOverride(self, status=SeriesStatus.DEVELOPMENT,1715 def setUpOverride(self, status=SeriesStatus.DEVELOPMENT,
1678 pocket=PackagePublishingPocket.RELEASE, binary=False,1716 pocket=PackagePublishingPocket.RELEASE, channel=None,
1679 ddeb=False, **kwargs):1717 binary=False, format=None, ddeb=False, **kwargs):
1680 self.distroseries.status = status1718 self.distroseries.status = status
1719 get_pub_kwargs = {"pocket": pocket, "channel": channel}
1720 if format is not None:
1721 get_pub_kwargs["format"] = format
1681 if ddeb:1722 if ddeb:
1682 pub = self.getPubBinaries(pocket=pocket, with_debug=True)[2]1723 pub = self.getPubBinaries(with_debug=True, **get_pub_kwargs)[2]
1683 self.assertEqual(1724 self.assertEqual(
1684 BinaryPackageFormat.DDEB,1725 BinaryPackageFormat.DDEB,
1685 pub.binarypackagerelease.binpackageformat)1726 pub.binarypackagerelease.binpackageformat)
1686 elif binary:1727 elif binary:
1687 pub = self.getPubBinaries(pocket=pocket)[0]1728 pub = self.getPubBinaries(**get_pub_kwargs)[0]
1688 else:1729 else:
1689 pub = self.getPubSource(pocket=pocket)1730 pub = self.getPubSource(**get_pub_kwargs)
1690 return pub.changeOverride(**kwargs)1731 return pub.changeOverride(**kwargs)
16911732
1692 def assertCanOverride(self, status=SeriesStatus.DEVELOPMENT,1733 def assertCanOverride(self, status=SeriesStatus.DEVELOPMENT,
1693 pocket=PackagePublishingPocket.RELEASE, **kwargs):1734 pocket=PackagePublishingPocket.RELEASE, channel=None,
1694 new_pub = self.setUpOverride(status=status, pocket=pocket, **kwargs)1735 **kwargs):
1736 new_pub = self.setUpOverride(
1737 status=status, pocket=pocket, channel=channel, **kwargs)
1695 self.assertEqual(new_pub.status, PackagePublishingStatus.PENDING)1738 self.assertEqual(new_pub.status, PackagePublishingStatus.PENDING)
1696 self.assertEqual(new_pub.pocket, pocket)1739 self.assertEqual(new_pub.pocket, pocket)
1740 self.assertEqual(new_pub.channel, channel)
1697 if "new_component" in kwargs:1741 if "new_component" in kwargs:
1698 self.assertEqual(kwargs["new_component"], new_pub.component.name)1742 self.assertEqual(kwargs["new_component"], new_pub.component.name)
1699 if "new_section" in kwargs:1743 if "new_section" in kwargs:
@@ -1784,6 +1828,12 @@ class TestChangeOverride(TestNativePublishingBase):
1784 self.assertCannotOverride(new_component="partner")1828 self.assertCannotOverride(new_component="partner")
1785 self.assertCannotOverride(binary=True, new_component="partner")1829 self.assertCannotOverride(binary=True, new_component="partner")
17861830
1831 def test_preserves_channel(self):
1832 self.assertCanOverride(
1833 binary=True, format=BinaryPackageFormat.WHL, channel="stable",
1834 new_component="universe", new_section="misc", new_priority="extra",
1835 new_phased_update_percentage=90)
1836
17871837
1788class TestPublishingHistoryView(TestCaseWithFactory):1838class TestPublishingHistoryView(TestCaseWithFactory):
1789 layer = LaunchpadFunctionalLayer1839 layer = LaunchpadFunctionalLayer
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 89925b6..29c6059 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -4029,6 +4029,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
4029 creator=None,4029 creator=None,
4030 packageupload=None,4030 packageupload=None,
4031 spr_creator=None,4031 spr_creator=None,
4032 channel=None,
4032 **kwargs):4033 **kwargs):
4033 """Make a `SourcePackagePublishingHistory`.4034 """Make a `SourcePackagePublishingHistory`.
40344035
@@ -4050,6 +4051,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
4050 is scheduled to be removed.4051 is scheduled to be removed.
4051 :param ancestor: The publication ancestor parameter.4052 :param ancestor: The publication ancestor parameter.
4052 :param creator: The publication creator.4053 :param creator: The publication creator.
4054 :param channel: An optional channel to publish into, as a string.
4053 :param **kwargs: All other parameters are passed through to the4055 :param **kwargs: All other parameters are passed through to the
4054 makeSourcePackageRelease call if needed.4056 makeSourcePackageRelease call if needed.
4055 """4057 """
@@ -4087,7 +4089,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
4087 archive, sourcepackagerelease, distroseries,4089 archive, sourcepackagerelease, distroseries,
4088 sourcepackagerelease.component, sourcepackagerelease.section,4090 sourcepackagerelease.component, sourcepackagerelease.section,
4089 pocket, ancestor=ancestor, creator=creator,4091 pocket, ancestor=ancestor, creator=creator,
4090 packageupload=packageupload)4092 packageupload=packageupload, channel=channel)
40914093
4092 naked_spph = removeSecurityProxy(spph)4094 naked_spph = removeSecurityProxy(spph)
4093 naked_spph.status = status4095 naked_spph.status = status
@@ -4113,7 +4115,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
4113 version=None,4115 version=None,
4114 architecturespecific=False,4116 architecturespecific=False,
4115 with_debug=False, with_file=False,4117 with_debug=False, with_file=False,
4116 creator=None):4118 creator=None, channel=None):
4117 """Make a `BinaryPackagePublishingHistory`."""4119 """Make a `BinaryPackagePublishingHistory`."""
4118 if distroarchseries is None:4120 if distroarchseries is None:
4119 if archive is None:4121 if archive is None:
@@ -4144,7 +4146,10 @@ class BareLaunchpadObjectFactory(ObjectFactory):
4144 if priority is None:4146 if priority is None:
4145 priority = PackagePublishingPriority.OPTIONAL4147 priority = PackagePublishingPriority.OPTIONAL
4146 if binpackageformat is None:4148 if binpackageformat is None:
4147 binpackageformat = BinaryPackageFormat.DEB4149 if binarypackagerelease is not None:
4150 binpackageformat = binarypackagerelease.binpackageformat
4151 else:
4152 binpackageformat = BinaryPackageFormat.DEB
41484153
4149 if binarypackagerelease is None:4154 if binarypackagerelease is None:
4150 # Create a new BinaryPackageBuild and BinaryPackageRelease4155 # Create a new BinaryPackageBuild and BinaryPackageRelease
@@ -4182,7 +4187,8 @@ class BareLaunchpadObjectFactory(ObjectFactory):
4182 archive, distroarchseries.distroseries, pocket,4187 archive, distroarchseries.distroseries, pocket,
4183 {binarypackagerelease: (4188 {binarypackagerelease: (
4184 binarypackagerelease.component, binarypackagerelease.section,4189 binarypackagerelease.component, binarypackagerelease.section,
4185 priority, None)})4190 priority, None)},
4191 channel=channel)
4186 for bpph in bpphs:4192 for bpph in bpphs:
4187 naked_bpph = removeSecurityProxy(bpph)4193 naked_bpph = removeSecurityProxy(bpph)
4188 naked_bpph.status = status4194 naked_bpph.status = status
@@ -4256,7 +4262,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
4256 libraryfile=library_file, filetype=filetype))4262 libraryfile=library_file, filetype=filetype))
42574263
4258 def makeBinaryPackageRelease(self, binarypackagename=None,4264 def makeBinaryPackageRelease(self, binarypackagename=None,
4259 version=None, build=None,4265 version=None, build=None, ci_build=None,
4260 binpackageformat=None, component=None,4266 binpackageformat=None, component=None,
4261 section_name=None, priority=None,4267 section_name=None, priority=None,
4262 architecturespecific=False,4268 architecturespecific=False,
@@ -4270,22 +4276,27 @@ class BareLaunchpadObjectFactory(ObjectFactory):
4270 date_created=None, debug_package=None,4276 date_created=None, debug_package=None,
4271 homepage=None):4277 homepage=None):
4272 """Make a `BinaryPackageRelease`."""4278 """Make a `BinaryPackageRelease`."""
4273 if build is None:4279 if build is None and ci_build is None:
4274 build = self.makeBinaryPackageBuild()4280 build = self.makeBinaryPackageBuild()
4275 if binarypackagename is None or isinstance(binarypackagename, str):4281 if binarypackagename is None or isinstance(binarypackagename, str):
4276 binarypackagename = self.getOrMakeBinaryPackageName(4282 binarypackagename = self.getOrMakeBinaryPackageName(
4277 binarypackagename)4283 binarypackagename)
4278 if version is None:4284 if version is None and build is not None:
4279 version = build.source_package_release.version4285 version = build.source_package_release.version
4280 if binpackageformat is None:4286 if binpackageformat is None:
4281 binpackageformat = BinaryPackageFormat.DEB4287 binpackageformat = BinaryPackageFormat.DEB
4282 if component is None:4288 if component is None and build is not None:
4283 component = build.source_package_release.component4289 component = build.source_package_release.component
4284 elif isinstance(component, str):4290 elif isinstance(component, str):
4285 component = getUtility(IComponentSet)[component]4291 component = getUtility(IComponentSet)[component]
4286 if isinstance(section_name, str):4292 if isinstance(section_name, str):
4287 section_name = self.makeSection(section_name)4293 section_name = self.makeSection(section_name)
4288 section = section_name or build.source_package_release.section4294 if section_name is not None:
4295 section = section_name
4296 elif build is not None:
4297 section = build.source_package_release.section
4298 else:
4299 section = None
4289 if priority is None:4300 if priority is None:
4290 priority = PackagePublishingPriority.OPTIONAL4301 priority = PackagePublishingPriority.OPTIONAL
4291 if summary is None:4302 if summary is None:
@@ -4294,18 +4305,35 @@ class BareLaunchpadObjectFactory(ObjectFactory):
4294 description = self.getUniqueString("description")4305 description = self.getUniqueString("description")
4295 if installed_size is None:4306 if installed_size is None:
4296 installed_size = self.getUniqueInteger()4307 installed_size = self.getUniqueInteger()
4297 bpr = build.createBinaryPackageRelease(4308 kwargs = {
4298 binarypackagename=binarypackagename, version=version,4309 "binarypackagename": binarypackagename,
4299 binpackageformat=binpackageformat,4310 "version": version,
4300 component=component, section=section, priority=priority,4311 "binpackageformat": binpackageformat,
4301 summary=summary, description=description,4312 "summary": summary,
4302 architecturespecific=architecturespecific,4313 "description": description,
4303 shlibdeps=shlibdeps, depends=depends, recommends=recommends,4314 "architecturespecific": architecturespecific,
4304 suggests=suggests, conflicts=conflicts, replaces=replaces,4315 "installedsize": installed_size,
4305 provides=provides, pre_depends=pre_depends,4316 "homepage": homepage,
4306 enhances=enhances, breaks=breaks, essential=essential,4317 }
4307 installedsize=installed_size, debug_package=debug_package,4318 if build is not None:
4308 homepage=homepage)4319 kwargs.update({
4320 "component": component,
4321 "section": section,
4322 "priority": priority,
4323 "shlibdeps": shlibdeps,
4324 "depends": depends,
4325 "recommends": recommends,
4326 "suggests": suggests,
4327 "conflicts": conflicts,
4328 "replaces": replaces,
4329 "provides": provides,
4330 "pre_depends": pre_depends,
4331 "enhances": enhances,
4332 "breaks": breaks,
4333 "essential": essential,
4334 "debug_package": debug_package,
4335 })
4336 bpr = (build or ci_build).createBinaryPackageRelease(**kwargs)
4309 if date_created is not None:4337 if date_created is not None:
4310 removeSecurityProxy(bpr).datecreated = date_created4338 removeSecurityProxy(bpr).datecreated = date_created
4311 return bpr4339 return bpr

Subscribers

People subscribed via source and target branches

to status/vote changes: