Merge lp:~jtv/launchpad/bug-884649-branch-3 into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Superseded
Proposed branch: lp:~jtv/launchpad/bug-884649-branch-3
Merge into: lp:launchpad
Diff against target: 722 lines (+357/-129)
2 files modified
lib/lp/archivepublisher/domination.py (+202/-101)
lib/lp/archivepublisher/tests/test_dominator.py (+155/-28)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-884649-branch-3
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+81134@code.launchpad.net

This proposal has been superseded by a proposal from 2011-11-03.

Commit message

Limit 2nd binary-domination pass to packages with arch-indep publications.

Description of the change

= Summary =

Domination is slow. A major reason is the two-pass domination algorithm needed for binary publications.

== Proposed fix ==

The first binary-domination pass touches only architecture-specific publications. This pass is relatively cheap, per package dominated.

The second pass touches only architecture-independent publications. This pass is expensive per package dominated. It probably dominates the same number of packages as the first pass, but for most, does nothing.

So: keep track during the first pass of which packages have architecture-independent publications, and limit the second pass to just those.

== Pre-implementation notes ==

This was Julian's idea.

== Implementation details ==

I made the second pass iterate over the intersection of “packages with multiple live publications” and “packages that were found during the first pass to have architecture-independent live publications.” This is because a package might conceivably have architecture-independent live publications in one architecture, but no live publications at all in another.

That's not really supposed to happen, which is to say it can be helpful to be prepared for the case but it's not worth optimizing for.

== Tests ==

All the high-level desired outcomes and integration are tested in scenario tests; those remain unchanged because this is a functionally neutral optimization.

I did add one helper function, which is short but easy to mess up and so it gets its own series of tests.

{{{
./bin/test -vvc lp.archivepublisher.tests.test_dominator
}}}

== Demo and Q/A ==

Run the dominator. It should be tons faster, but still dominate even architecture-independent binary publications.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/archivepublisher/domination.py
  lib/lp/archivepublisher/tests/test_dominator.py

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/archivepublisher/domination.py'
--- lib/lp/archivepublisher/domination.py 2011-11-02 12:58:31 +0000
+++ lib/lp/archivepublisher/domination.py 2011-11-03 11:52:37 +0000
@@ -52,8 +52,12 @@
5252
53__all__ = ['Dominator']53__all__ = ['Dominator']
5454
55from collections import defaultdict
55from datetime import timedelta56from datetime import timedelta
56from operator import itemgetter57from operator import (
58 attrgetter,
59 itemgetter,
60 )
5761
58import apt_pkg62import apt_pkg
59from storm.expr import (63from storm.expr import (
@@ -72,6 +76,7 @@
72 DecoratedResultSet,76 DecoratedResultSet,
73 )77 )
74from canonical.launchpad.interfaces.lpstorm import IStore78from canonical.launchpad.interfaces.lpstorm import IStore
79from canonical.launchpad.utilities.orderingcheck import OrderingCheck
75from lp.registry.model.sourcepackagename import SourcePackageName80from lp.registry.model.sourcepackagename import SourcePackageName
76from lp.services.database.bulk import load_related81from lp.services.database.bulk import load_related
77from lp.soyuz.enums import (82from lp.soyuz.enums import (
@@ -192,6 +197,109 @@
192 else:197 else:
193 return version_comparison198 return version_comparison
194199
200 def sortPublications(self, publications):
201 """Sort publications from most to least current versions."""
202 # Listify; we want to iterate this twice, which won't do for a
203 # non-persistent sequence.
204 sorted_publications = list(publications)
205 # Batch-load associated package releases; we'll be needing them
206 # to compare versions.
207 self.load_releases(sorted_publications)
208 # Now sort. This is that second iteration. An in-place sort
209 # won't hurt the original, because we're working on a copy of
210 # the original iterable.
211 sorted_publications.sort(cmp=self.compare, reverse=True)
212 return sorted_publications
213
214
215def find_live_source_versions(publications):
216 """Find versions out of Published `publications` that should stay live.
217
218 This particular notion of liveness applies to source domination: the
219 latest version stays live, and that's it.
220
221 :param publications: An iterable of `SourcePackagePublishingHistory`
222 sorted by descending package version.
223 :return: A list of live versions.
224 """
225 # Given the required sort order, the latest version is at the head
226 # of the list.
227 return [publications[0].sourcepackagerelease.version]
228
229
230def get_binary_versions(binary_publications):
231 """List versions for sequence of `BinaryPackagePublishingHistory`."""
232 return [pub.binarypackagerelease.version for pub in binary_publications]
233
234
235def find_live_binary_versions_pass_1(publications):
236 """Find versions out of Published `publications` that should stay live.
237
238 This particular notion of liveness applies to first-pass binary
239 domination: the latest version stays live, and so do publications of
240 binary packages for the "all" architecture.
241
242 :param publications: An iterable of `BinaryPackagePublishingHistory`,
243 sorted by descending package version.
244 :return: A list of live versions.
245 """
246 publications = list(publications)
247 latest = publications.pop(0)
248 return get_binary_versions(
249 [latest] + [
250 pub for pub in publications if not pub.architecture_specific])
251
252
253def find_live_binary_versions_pass_2(publications):
254 """Find versions out of Published `publications` that should stay live.
255
256 This particular notion of liveness applies to second-pass binary
257 domination: the latest version stays live, and architecture-specific
258 publications stay live (i.e, ones that are not for the "all"
259 architecture).
260
261 More importantly, any publication for binary packages of the "all"
262 architecture stay live if any of the non-"all" binary packages from
263 the same source package release are still active -- even if they are
264 for other architectures.
265
266 This is the raison d'etre for the two-pass binary domination algorithm:
267 to let us see which architecture-independent binary publications can be
268 superseded without rendering any architecture-specific binaries from the
269 same source package release uninstallable.
270
271 (Note that here, "active" includes Published publications but also
272 Pending ones. This is standard nomenclature in Soyuz. Some of the
273 domination code confuses matters by using the term "active" to mean only
274 Published publications).
275
276 :param publications: An iterable of `BinaryPackagePublishingHistory`,
277 sorted by descending package version.
278 :return: A list of live versions.
279 """
280 publications = list(publications)
281 latest = publications.pop(0)
282 is_arch_specific = attrgetter('architecture_specific')
283 arch_specific_pubs = filter(is_arch_specific, publications)
284 arch_indep_pubs = filter(
285 lambda pub: not is_arch_specific(pub),
286 publications)
287
288 # XXX JeroenVermeulen 2011-11-01 bug=884649: This is likely to be
289 # costly, and the result could be reused for all builds of the same
290 # source package release to all architectures.
291 reprieved_pubs = [
292 pub
293 for pub in arch_indep_pubs
294 if pub.getOtherPublicationsForSameSource().any()]
295
296 return get_binary_versions([latest] + arch_specific_pubs + reprieved_pubs)
297
298
299def contains_arch_indep(bpphs):
300 """Are any of the publications among `bpphs` architecture-independent?"""
301 return any(not bpph.architecture_specific for bpph in bpphs)
302
195303
196class Dominator:304class Dominator:
197 """Manage the process of marking packages as superseded.305 """Manage the process of marking packages as superseded.
@@ -209,27 +317,6 @@
209 self.logger = logger317 self.logger = logger
210 self.archive = archive318 self.archive = archive
211319
212 def _checkArchIndep(self, publication):
213 """Return True if the binary publication can be superseded.
214
215 If the publication is an arch-indep binary, we can only supersede
216 it if all the binaries from the same source are also superseded,
217 else those binaries may become uninstallable.
218 See bug 34086.
219 """
220 binary = publication.binarypackagerelease
221 if not binary.architecturespecific:
222 # getOtherPublicationsForSameSource returns PENDING in
223 # addition to PUBLISHED binaries, and we rely on this since
224 # they must also block domination.
225 others = publication.getOtherPublicationsForSameSource()
226 if others.any():
227 # Don't dominate this arch:all binary as there are
228 # other arch-specific binaries from the same build
229 # that are still active.
230 return False
231 return True
232
233 def dominatePackage(self, publications, live_versions, generalization):320 def dominatePackage(self, publications, live_versions, generalization):
234 """Dominate publications for a single package.321 """Dominate publications for a single package.
235322
@@ -247,34 +334,33 @@
247334
248 :param publications: Iterable of publications for the same package,335 :param publications: Iterable of publications for the same package,
249 in the same archive, series, and pocket, all with status336 in the same archive, series, and pocket, all with status
250 `PackagePublishingStatus.PUBLISHED`.337 `PackagePublishingStatus.PUBLISHED`. They must be sorted from
251 :param live_versions: Iterable of version strings that are still338 most current to least current, as would be the result of
252 considered live for this package. The given publications will339 `generalization.sortPublications`.
253 remain active insofar as they represent any of these versions;340 :param live_versions: Iterable of versions that are still considered
254 older publications will be marked as superseded and newer ones341 "live" for this package. For any of these, the latest publication
255 as deleted.342 among `publications` will remain Published. Publications for
343 older releases, as well as older publications of live versions,
344 will be marked as Superseded. Publications of newer versions than
345 are listed in `live_versions` are marked as Deleted.
256 :param generalization: A `GeneralizedPublication` helper representing346 :param generalization: A `GeneralizedPublication` helper representing
257 the kind of publications these are--source or binary.347 the kind of publications these are: source or binary.
258 """348 """
259 publications = list(publications)349 live_versions = frozenset(live_versions)
260 generalization.load_releases(publications)
261
262 # Go through publications from latest version to oldest. This
263 # makes it easy to figure out which release superseded which:
264 # the dominant is always the oldest live release that is newer
265 # than the one being superseded. In this loop, that means the
266 # dominant is always the last live publication we saw.
267 publications = sorted(
268 publications, cmp=generalization.compare, reverse=True)
269350
270 self.logger.debug(351 self.logger.debug(
271 "Package has %d live publication(s). Live versions: %s",352 "Package has %d live publication(s). Live versions: %s",
272 len(publications), live_versions)353 len(publications), live_versions)
273354
355 # Verify that the publications are really sorted properly.
356 check_order = OrderingCheck(cmp=generalization.compare, reverse=True)
357
274 current_dominant = None358 current_dominant = None
275 dominant_version = None359 dominant_version = None
276360
277 for pub in publications:361 for pub in publications:
362 check_order.check(pub)
363
278 version = generalization.getPackageVersion(pub)364 version = generalization.getPackageVersion(pub)
279 # There should never be two published releases with the same365 # There should never be two published releases with the same
280 # version. So it doesn't matter whether this comparison is366 # version. So it doesn't matter whether this comparison is
@@ -295,11 +381,6 @@
295 current_dominant = pub381 current_dominant = pub
296 dominant_version = version382 dominant_version = version
297 self.logger.debug2("Keeping version %s.", version)383 self.logger.debug2("Keeping version %s.", version)
298 elif not (generalization.is_source or self._checkArchIndep(pub)):
299 # As a special case, we keep this version live as well.
300 current_dominant = pub
301 dominant_version = version
302 self.logger.debug2("Keeping version %s.", version)
303 elif current_dominant is None:384 elif current_dominant is None:
304 # This publication is no longer live, but there is no385 # This publication is no longer live, but there is no
305 # newer version to supersede it either. Therefore it386 # newer version to supersede it either. Therefore it
@@ -312,50 +393,32 @@
312 pub.supersede(current_dominant, logger=self.logger)393 pub.supersede(current_dominant, logger=self.logger)
313 self.logger.debug2("Superseding version %s.", version)394 self.logger.debug2("Superseding version %s.", version)
314395
315 def _dominatePublications(self, pubs, generalization):396 def _sortPackages(self, publications, generalization):
316 """Perform dominations for the given publications.397 """Partition publications by package name, and sort them.
317398
318 Keep the latest published version for each package active,399 The publications are sorted from most current to least current,
319 superseding older versions.400 as required by `dominatePackage` etc.
320401
321 :param pubs: A dict mapping names to a list of publications. Every402 :param publications: An iterable of `SourcePackagePublishingHistory`
322 publication must be PUBLISHED or PENDING, and the first in each403 or of `BinaryPackagePublishingHistory`.
323 list will be treated as dominant (so should be the latest).404 :param generalization: A `GeneralizedPublication` helper representing
324 :param generalization: A `GeneralizedPublication` helper representing405 the kind of publications these are: source or binary.
325 the kind of publications these are--source or binary.406 :return: A dict mapping each package name to a sorted list of
326 """407 publications from `publications`.
327 self.logger.debug("Dominating packages...")408 """
328 for name, publications in pubs.iteritems():409 pubs_by_package = defaultdict(list)
329 assert publications, "Empty list of publications for %s." % name410 for pub in publications:
330 # Since this always picks the latest version as the live411 pubs_by_package[generalization.getPackageName(pub)].append(pub)
331 # one, this dominatePackage call will never result in a412
332 # deletion.413 # Sort the publication lists. This is not an in-place sort, so
333 latest_version = generalization.getPackageVersion(publications[0])414 # it involves altering the dict while we iterate it. Listify
334 self.logger.debug2("Dominating %s" % name)415 # the keys so that we can be sure that we're not altering the
335 self.dominatePackage(416 # iteration order while iteration is underway.
336 publications, [latest_version], generalization)417 for package in list(pubs_by_package.keys()):
337418 pubs_by_package[package] = generalization.sortPublications(
338 def _sortPackages(self, pkglist, generalization):419 pubs_by_package[package])
339 """Map out packages by name, and sort by descending version.420
340421 return pubs_by_package
341 :param pkglist: An iterable of `SourcePackagePublishingHistory` or
342 `BinaryPackagePublishingHistory`.
343 :param generalization: A `GeneralizedPublication` helper representing
344 the kind of publications these are--source or binary.
345 :return: A dict mapping each package name to a list of publications
346 from `pkglist`, newest first.
347 """
348 self.logger.debug("Sorting packages...")
349
350 outpkgs = {}
351 for inpkg in pkglist:
352 key = generalization.getPackageName(inpkg)
353 outpkgs.setdefault(key, []).append(inpkg)
354
355 for package_pubs in outpkgs.itervalues():
356 package_pubs.sort(cmp=generalization.compare, reverse=True)
357
358 return outpkgs
359422
360 def _setScheduledDeletionDate(self, pub_record):423 def _setScheduledDeletionDate(self, pub_record):
361 """Set the scheduleddeletiondate on a publishing record.424 """Set the scheduleddeletiondate on a publishing record.
@@ -510,6 +573,18 @@
510 """573 """
511 generalization = GeneralizedPublication(is_source=False)574 generalization = GeneralizedPublication(is_source=False)
512575
576 # Domination happens in two passes. The first tries to
577 # supersede architecture-dependent publications; the second
578 # tries to supersede architecture-independent ones. An
579 # architecture-independent pub is kept alive as long as any
580 # architecture-dependent pubs from the same source package build
581 # are still live for any architecture, because they may depend
582 # on the architecture-independent package.
583 # Thus we limit the second pass to those packages that have
584 # published, architecture-independent publications; anything
585 # else will have completed domination in the first pass.
586 packages_w_arch_indep = set()
587
513 for distroarchseries in distroseries.architectures:588 for distroarchseries in distroseries.architectures:
514 self.logger.info(589 self.logger.info(
515 "Performing domination across %s/%s (%s)",590 "Performing domination across %s/%s (%s)",
@@ -520,21 +595,34 @@
520 bins = self.findBinariesForDomination(distroarchseries, pocket)595 bins = self.findBinariesForDomination(distroarchseries, pocket)
521 sorted_packages = self._sortPackages(bins, generalization)596 sorted_packages = self._sortPackages(bins, generalization)
522 self.logger.info("Dominating binaries...")597 self.logger.info("Dominating binaries...")
523 self._dominatePublications(sorted_packages, generalization)598 for name, pubs in sorted_packages.iteritems():
524599 self.logger.debug("Dominating %s" % name)
525 # We need to make a second pass to cover the cases where:600 assert len(pubs) > 0, "Dominating zero binaries!"
526 # * arch-specific binaries were not all dominated before arch-all601 live_versions = find_live_binary_versions_pass_1(pubs)
527 # ones that depend on them602 self.dominatePackage(pubs, live_versions, generalization)
528 # * An arch-all turned into an arch-specific, or vice-versa603 if contains_arch_indep(pubs):
529 # * A package is completely schizophrenic and changes all of604 packages_w_arch_indep.add(name)
530 # its binaries between arch-all and arch-any (apparently605
531 # occurs sometimes!)606 packages_w_arch_indep = frozenset(packages_w_arch_indep)
607
608 # The second pass attempts to supersede arch-all publications of
609 # older versions, from source package releases that no longer
610 # have any active arch-specific publications that might depend
611 # on the arch-indep ones.
612 # (In maintaining this code, bear in mind that some or all of a
613 # source package's binary packages may switch between
614 # arch-specific and arch-indep between releases.)
532 for distroarchseries in distroseries.architectures:615 for distroarchseries in distroseries.architectures:
533 self.logger.info("Finding binaries...(2nd pass)")616 self.logger.info("Finding binaries...(2nd pass)")
534 bins = self.findBinariesForDomination(distroarchseries, pocket)617 bins = self.findBinariesForDomination(distroarchseries, pocket)
535 sorted_packages = self._sortPackages(bins, generalization)618 sorted_packages = self._sortPackages(bins, generalization)
536 self.logger.info("Dominating binaries...(2nd pass)")619 self.logger.info("Dominating binaries...(2nd pass)")
537 self._dominatePublications(sorted_packages, generalization)620 for name in packages_w_arch_indep.intersection(sorted_packages):
621 pubs = sorted_packages[name]
622 self.logger.debug("Dominating %s" % name)
623 assert len(pubs) > 0, "Dominating zero binaries in 2nd pass!"
624 live_versions = find_live_binary_versions_pass_2(pubs)
625 self.dominatePackage(pubs, live_versions, generalization)
538626
539 def _composeActiveSourcePubsCondition(self, distroseries, pocket):627 def _composeActiveSourcePubsCondition(self, distroseries, pocket):
540 """Compose ORM condition for restricting relevant source pubs."""628 """Compose ORM condition for restricting relevant source pubs."""
@@ -550,7 +638,12 @@
550 )638 )
551639
552 def findSourcesForDomination(self, distroseries, pocket):640 def findSourcesForDomination(self, distroseries, pocket):
553 """Find binary publications that need dominating."""641 """Find binary publications that need dominating.
642
643 This is only for traditional domination, where the latest published
644 publication is always kept published. It will ignore publications
645 that have no other publications competing for the same binary package.
646 """
554 # Avoid circular imports.647 # Avoid circular imports.
555 from lp.soyuz.model.publishing import SourcePackagePublishingHistory648 from lp.soyuz.model.publishing import SourcePackagePublishingHistory
556649
@@ -589,11 +682,18 @@
589 distroseries.name, pocket.title)682 distroseries.name, pocket.title)
590683
591 generalization = GeneralizedPublication(is_source=True)684 generalization = GeneralizedPublication(is_source=True)
685
686 self.logger.debug("Finding sources...")
592 sources = self.findSourcesForDomination(distroseries, pocket)687 sources = self.findSourcesForDomination(distroseries, pocket)
688 sorted_packages = self._sortPackages(sources, generalization)
593689
594 self.logger.debug("Dominating sources...")690 self.logger.debug("Dominating sources...")
595 self._dominatePublications(691 for name, pubs in sorted_packages.iteritems():
596 self._sortPackages(sources, generalization), generalization)692 self.logger.debug("Dominating %s" % name)
693 assert len(pubs) > 0, "Dominating zero binaries!"
694 live_versions = find_live_source_versions(pubs)
695 self.dominatePackage(pubs, live_versions, generalization)
696
597 flush_database_updates()697 flush_database_updates()
598698
599 def findPublishedSourcePackageNames(self, distroseries, pocket):699 def findPublishedSourcePackageNames(self, distroseries, pocket):
@@ -653,6 +753,7 @@
653 """753 """
654 generalization = GeneralizedPublication(is_source=True)754 generalization = GeneralizedPublication(is_source=True)
655 pubs = self.findPublishedSPPHs(distroseries, pocket, package_name)755 pubs = self.findPublishedSPPHs(distroseries, pocket, package_name)
756 pubs = generalization.sortPublications(pubs)
656 self.dominatePackage(pubs, live_versions, generalization)757 self.dominatePackage(pubs, live_versions, generalization)
657758
658 def judge(self, distroseries, pocket):759 def judge(self, distroseries, pocket):
659760
=== modified file 'lib/lp/archivepublisher/tests/test_dominator.py'
--- lib/lp/archivepublisher/tests/test_dominator.py 2011-11-02 10:28:31 +0000
+++ lib/lp/archivepublisher/tests/test_dominator.py 2011-11-03 11:52:37 +0000
@@ -15,7 +15,11 @@
15from canonical.database.sqlbase import flush_database_updates15from canonical.database.sqlbase import flush_database_updates
16from canonical.testing.layers import ZopelessDatabaseLayer16from canonical.testing.layers import ZopelessDatabaseLayer
17from lp.archivepublisher.domination import (17from lp.archivepublisher.domination import (
18 contains_arch_indep,
18 Dominator,19 Dominator,
20 find_live_binary_versions_pass_1,
21 find_live_binary_versions_pass_2,
22 find_live_source_versions,
19 GeneralizedPublication,23 GeneralizedPublication,
20 STAY_OF_EXECUTION,24 STAY_OF_EXECUTION,
21 )25 )
@@ -30,6 +34,7 @@
30 StormStatementRecorder,34 StormStatementRecorder,
31 TestCaseWithFactory,35 TestCaseWithFactory,
32 )36 )
37from lp.testing.fakemethod import FakeMethod
33from lp.testing.matchers import HasQueryCount38from lp.testing.matchers import HasQueryCount
3439
3540
@@ -72,13 +77,9 @@
72 is_source=ISourcePackagePublishingHistory.providedBy(dominant))77 is_source=ISourcePackagePublishingHistory.providedBy(dominant))
73 dominator = Dominator(self.logger, self.ubuntutest.main_archive)78 dominator = Dominator(self.logger, self.ubuntutest.main_archive)
7479
75 # The _dominate* test methods require a dictionary where the80 pubs = [dominant, dominated]
76 # package name is the key. The key's value is a list of81 live_versions = [generalization.getPackageVersion(dominant)]
77 # source or binary packages representing dominant, the first element82 dominator.dominatePackage(pubs, live_versions, generalization)
78 # and dominated, the subsequents.
79 pubs = {'foo': [dominant, dominated]}
80
81 dominator._dominatePublications(pubs, generalization)
82 flush_database_updates()83 flush_database_updates()
8384
84 # The dominant version remains correctly published.85 # The dominant version remains correctly published.
@@ -158,16 +159,30 @@
158 [foo_10_source] + foo_10_binaries,159 [foo_10_source] + foo_10_binaries,
159 PackagePublishingStatus.SUPERSEDED)160 PackagePublishingStatus.SUPERSEDED)
160161
161 def testEmptyDomination(self):162 def test_dominateBinaries_rejects_empty_publication_list(self):
162 """Domination asserts for not empty input list."""163 """Domination asserts for non-empty input list."""
163 dominator = Dominator(self.logger, self.ubuntutest.main_archive)164 package = self.factory.makeBinaryPackageName()
164 pubs = {'foo': []}165 dominator = Dominator(self.logger, self.ubuntutest.main_archive)
165 # This isn't a really good exception. It should probably be166 dominator._sortPackages = FakeMethod({package.name: []})
166 # something more indicative of bad input.167 # This isn't a really good exception. It should probably be
167 self.assertRaises(168 # something more indicative of bad input.
168 AssertionError,169 self.assertRaises(
169 dominator._dominatePublications,170 AssertionError,
170 pubs, GeneralizedPublication(True))171 dominator.dominateBinaries,
172 self.factory.makeDistroArchSeries().distroseries,
173 self.factory.getAnyPocket())
174
175 def test_dominateSources_rejects_empty_publication_list(self):
176 """Domination asserts for non-empty input list."""
177 package = self.factory.makeSourcePackageName()
178 dominator = Dominator(self.logger, self.ubuntutest.main_archive)
179 dominator._sortPackages = FakeMethod({package.name: []})
180 # This isn't a really good exception. It should probably be
181 # something more indicative of bad input.
182 self.assertRaises(
183 AssertionError,
184 dominator.dominateSources,
185 self.factory.makeDistroSeries(), self.factory.getAnyPocket())
171186
172 def test_archall_domination(self):187 def test_archall_domination(self):
173 # Arch-all binaries should not be dominated when a new source188 # Arch-all binaries should not be dominated when a new source
@@ -358,6 +373,16 @@
358 SeriesStatus.OBSOLETE)373 SeriesStatus.OBSOLETE)
359374
360375
376def remove_security_proxies(proxied_objects):
377 """Return list of `proxied_objects`, without their proxies.
378
379 The dominator runs only in scripts, where security proxies don't get
380 in the way. To test realistically for this environment, strip the
381 proxies wherever necessary and do as you will.
382 """
383 return [removeSecurityProxy(obj) for obj in proxied_objects]
384
385
361def make_spphs_for_versions(factory, versions):386def make_spphs_for_versions(factory, versions):
362 """Create publication records for each of `versions`.387 """Create publication records for each of `versions`.
363388
@@ -400,14 +425,15 @@
400 archive = das.distroseries.main_archive425 archive = das.distroseries.main_archive
401 pocket = factory.getAnyPocket()426 pocket = factory.getAnyPocket()
402 bprs = [427 bprs = [
403 factory.makeBinaryPackageRelease(binarypackagename=bpn)428 factory.makeBinaryPackageRelease(
429 binarypackagename=bpn, version=version)
404 for version in versions]430 for version in versions]
405 return [431 return remove_security_proxies([
406 factory.makeBinaryPackagePublishingHistory(432 factory.makeBinaryPackagePublishingHistory(
407 binarypackagerelease=bpr, binarypackagename=bpn,433 binarypackagerelease=bpr, binarypackagename=bpn,
408 distroarchseries=das, pocket=pocket, archive=archive,434 distroarchseries=das, pocket=pocket, archive=archive,
409 sourcepackagename=spn, status=PackagePublishingStatus.PUBLISHED)435 sourcepackagename=spn, status=PackagePublishingStatus.PUBLISHED)
410 for bpr in bprs]436 for bpr in bprs])
411437
412438
413def list_source_versions(spphs):439def list_source_versions(spphs):
@@ -591,9 +617,10 @@
591 def test_dominatePackage_supersedes_older_pub_with_newer_live_pub(self):617 def test_dominatePackage_supersedes_older_pub_with_newer_live_pub(self):
592 # When marking a package as superseded, dominatePackage618 # When marking a package as superseded, dominatePackage
593 # designates a newer live version as the superseding version.619 # designates a newer live version as the superseding version.
620 generalization = GeneralizedPublication(True)
594 pubs = make_spphs_for_versions(self.factory, ['1.0', '1.1'])621 pubs = make_spphs_for_versions(self.factory, ['1.0', '1.1'])
595 self.makeDominator(pubs).dominatePackage(622 self.makeDominator(pubs).dominatePackage(
596 pubs, ['1.1'], GeneralizedPublication(True))623 generalization.sortPublications(pubs), ['1.1'], generalization)
597 self.assertEqual(PackagePublishingStatus.SUPERSEDED, pubs[0].status)624 self.assertEqual(PackagePublishingStatus.SUPERSEDED, pubs[0].status)
598 self.assertEqual(pubs[1].sourcepackagerelease, pubs[0].supersededby)625 self.assertEqual(pubs[1].sourcepackagerelease, pubs[0].supersededby)
599 self.assertEqual(PackagePublishingStatus.PUBLISHED, pubs[1].status)626 self.assertEqual(PackagePublishingStatus.PUBLISHED, pubs[1].status)
@@ -601,10 +628,11 @@
601 def test_dominatePackage_only_supersedes_with_live_pub(self):628 def test_dominatePackage_only_supersedes_with_live_pub(self):
602 # When marking a package as superseded, dominatePackage will629 # When marking a package as superseded, dominatePackage will
603 # only pick a live version as the superseding one.630 # only pick a live version as the superseding one.
631 generalization = GeneralizedPublication(True)
604 pubs = make_spphs_for_versions(632 pubs = make_spphs_for_versions(
605 self.factory, ['1.0', '2.0', '3.0', '4.0'])633 self.factory, ['1.0', '2.0', '3.0', '4.0'])
606 self.makeDominator(pubs).dominatePackage(634 self.makeDominator(pubs).dominatePackage(
607 pubs, ['3.0'], GeneralizedPublication(True))635 generalization.sortPublications(pubs), ['3.0'], generalization)
608 self.assertEqual([636 self.assertEqual([
609 pubs[2].sourcepackagerelease,637 pubs[2].sourcepackagerelease,
610 pubs[2].sourcepackagerelease,638 pubs[2].sourcepackagerelease,
@@ -616,23 +644,27 @@
616 def test_dominatePackage_supersedes_with_oldest_newer_live_pub(self):644 def test_dominatePackage_supersedes_with_oldest_newer_live_pub(self):
617 # When marking a package as superseded, dominatePackage picks645 # When marking a package as superseded, dominatePackage picks
618 # the oldest of the newer, live versions as the superseding one.646 # the oldest of the newer, live versions as the superseding one.
647 generalization = GeneralizedPublication(True)
619 pubs = make_spphs_for_versions(self.factory, ['2.7', '2.8', '2.9'])648 pubs = make_spphs_for_versions(self.factory, ['2.7', '2.8', '2.9'])
620 self.makeDominator(pubs).dominatePackage(649 self.makeDominator(pubs).dominatePackage(
621 pubs, ['2.8', '2.9'], GeneralizedPublication(True))650 generalization.sortPublications(pubs), ['2.8', '2.9'],
651 generalization)
622 self.assertEqual(pubs[1].sourcepackagerelease, pubs[0].supersededby)652 self.assertEqual(pubs[1].sourcepackagerelease, pubs[0].supersededby)
623653
624 def test_dominatePackage_only_supersedes_with_newer_live_pub(self):654 def test_dominatePackage_only_supersedes_with_newer_live_pub(self):
625 # When marking a package as superseded, dominatePackage only655 # When marking a package as superseded, dominatePackage only
626 # considers a newer version as the superseding one.656 # considers a newer version as the superseding one.
657 generalization = GeneralizedPublication(True)
627 pubs = make_spphs_for_versions(self.factory, ['0.1', '0.2'])658 pubs = make_spphs_for_versions(self.factory, ['0.1', '0.2'])
628 self.makeDominator(pubs).dominatePackage(659 self.makeDominator(pubs).dominatePackage(
629 pubs, ['0.1'], GeneralizedPublication(True))660 generalization.sortPublications(pubs), ['0.1'], generalization)
630 self.assertEqual(None, pubs[1].supersededby)661 self.assertEqual(None, pubs[1].supersededby)
631 self.assertEqual(PackagePublishingStatus.DELETED, pubs[1].status)662 self.assertEqual(PackagePublishingStatus.DELETED, pubs[1].status)
632663
633 def test_dominatePackage_supersedes_replaced_pub_for_live_version(self):664 def test_dominatePackage_supersedes_replaced_pub_for_live_version(self):
634 # Even if a publication record is for a live version, a newer665 # Even if a publication record is for a live version, a newer
635 # one for the same version supersedes it.666 # one for the same version supersedes it.
667 generalization = GeneralizedPublication(True)
636 spr = self.factory.makeSourcePackageRelease()668 spr = self.factory.makeSourcePackageRelease()
637 series = self.factory.makeDistroSeries()669 series = self.factory.makeDistroSeries()
638 pocket = PackagePublishingPocket.RELEASE670 pocket = PackagePublishingPocket.RELEASE
@@ -649,7 +681,8 @@
649 ])681 ])
650682
651 self.makeDominator(pubs).dominatePackage(683 self.makeDominator(pubs).dominatePackage(
652 pubs, [spr.version], GeneralizedPublication(True))684 generalization.sortPublications(pubs), [spr.version],
685 generalization)
653 self.assertEqual([686 self.assertEqual([
654 PackagePublishingStatus.SUPERSEDED,687 PackagePublishingStatus.SUPERSEDED,
655 PackagePublishingStatus.SUPERSEDED,688 PackagePublishingStatus.SUPERSEDED,
@@ -661,12 +694,13 @@
661694
662 def test_dominatePackage_is_efficient(self):695 def test_dominatePackage_is_efficient(self):
663 # dominatePackage avoids issuing too many queries.696 # dominatePackage avoids issuing too many queries.
697 generalization = GeneralizedPublication(True)
664 versions = ["1.%s" % revision for revision in xrange(5)]698 versions = ["1.%s" % revision for revision in xrange(5)]
665 pubs = make_spphs_for_versions(self.factory, versions)699 pubs = make_spphs_for_versions(self.factory, versions)
666 with StormStatementRecorder() as recorder:700 with StormStatementRecorder() as recorder:
667 self.makeDominator(pubs).dominatePackage(701 self.makeDominator(pubs).dominatePackage(
668 pubs, versions[2:-1],702 generalization.sortPublications(pubs), versions[2:-1],
669 GeneralizedPublication(True))703 generalization)
670 self.assertThat(recorder, HasQueryCount(LessThan(5)))704 self.assertThat(recorder, HasQueryCount(LessThan(5)))
671705
672 def test_dominatePackage_advanced_scenario(self):706 def test_dominatePackage_advanced_scenario(self):
@@ -677,6 +711,7 @@
677 # don't just patch up the code or this test. Create unit tests711 # don't just patch up the code or this test. Create unit tests
678 # that specifically cover the difference, then change the code712 # that specifically cover the difference, then change the code
679 # and/or adapt this test to return to harmony.713 # and/or adapt this test to return to harmony.
714 generalization = GeneralizedPublication(True)
680 series = self.factory.makeDistroSeries()715 series = self.factory.makeDistroSeries()
681 package = self.factory.makeSourcePackageName()716 package = self.factory.makeSourcePackageName()
682 pocket = PackagePublishingPocket.RELEASE717 pocket = PackagePublishingPocket.RELEASE
@@ -723,7 +758,8 @@
723758
724 all_pubs = sum(pubs_by_version.itervalues(), [])759 all_pubs = sum(pubs_by_version.itervalues(), [])
725 Dominator(DevNullLogger(), series.main_archive).dominatePackage(760 Dominator(DevNullLogger(), series.main_archive).dominatePackage(
726 all_pubs, live_versions, GeneralizedPublication(True))761 generalization.sortPublications(all_pubs), live_versions,
762 generalization)
727763
728 for version in reversed(versions):764 for version in reversed(versions):
729 pubs = pubs_by_version[version]765 pubs = pubs_by_version[version]
@@ -1089,3 +1125,94 @@
1089 published_spphs,1125 published_spphs,
1090 dominator.findSourcesForDomination(1126 dominator.findSourcesForDomination(
1091 spphs[0].distroseries, spphs[0].pocket))1127 spphs[0].distroseries, spphs[0].pocket))
1128
1129
1130def make_publications_arch_specific(pubs, arch_specific=True):
1131 """Set the `architecturespecific` attribute for given SPPHs.
1132
1133 :param pubs: An iterable of `BinaryPackagePublishingHistory`.
1134 :param arch_specific: Whether the binary package releases published
1135 by `pubs` are to be architecture-specific. If not, they will be
1136 treated as being for the "all" architecture.
1137 """
1138 for pub in pubs:
1139 bpr = removeSecurityProxy(pub).binarypackagerelease
1140 bpr.architecturespecific = arch_specific
1141
1142
1143class TestLivenessFunctions(TestCaseWithFactory):
1144 """Tests for the functions that say which versions are live."""
1145
1146 layer = ZopelessDatabaseLayer
1147
1148 def test_find_live_source_versions_blesses_latest(self):
1149 spphs = make_spphs_for_versions(self.factory, ['1.2', '1.1', '1.0'])
1150 self.assertEqual(['1.2'], find_live_source_versions(spphs))
1151
1152 def test_find_live_binary_versions_pass_1_blesses_latest(self):
1153 bpphs = make_bpphs_for_versions(self.factory, ['1.2', '1.1', '1.0'])
1154 make_publications_arch_specific(bpphs)
1155 self.assertEqual(['1.2'], find_live_binary_versions_pass_1(bpphs))
1156
1157 def test_find_live_binary_versions_pass_1_blesses_arch_all(self):
1158 versions = list(reversed(['1.%d' % version for version in range(3)]))
1159 bpphs = make_bpphs_for_versions(self.factory, versions)
1160
1161 # All of these publications are architecture-specific, except
1162 # the last one. This would happen if the binary package had
1163 # just changed from being architecture-specific to being
1164 # architecture-independent.
1165 make_publications_arch_specific(bpphs, True)
1166 make_publications_arch_specific(bpphs[-1:], False)
1167 self.assertEqual(
1168 versions[:1] + versions[-1:],
1169 find_live_binary_versions_pass_1(bpphs))
1170
1171 def test_find_live_binary_versions_pass_2_blesses_latest(self):
1172 bpphs = make_bpphs_for_versions(self.factory, ['1.2', '1.1', '1.0'])
1173 make_publications_arch_specific(bpphs, False)
1174 self.assertEqual(['1.2'], find_live_binary_versions_pass_2(bpphs))
1175
1176 def test_find_live_binary_versions_pass_2_blesses_arch_specific(self):
1177 versions = list(reversed(['1.%d' % version for version in range(3)]))
1178 bpphs = make_bpphs_for_versions(self.factory, versions)
1179 make_publications_arch_specific(bpphs)
1180 self.assertEqual(versions, find_live_binary_versions_pass_2(bpphs))
1181
1182 def test_find_live_binary_versions_pass_2_reprieves_arch_all(self):
1183 # An arch-all BPPH for a BPR built by an SPR that also still has
1184 # active arch-dependent BPPHs gets a reprieve: it can't be
1185 # superseded until those arch-dependent BPPHs have been
1186 # superseded.
1187 bpphs = make_bpphs_for_versions(self.factory, ['1.2', '1.1', '1.0'])
1188 make_publications_arch_specific(bpphs, False)
1189 dependent = self.factory.makeBinaryPackagePublishingHistory(
1190 binarypackagerelease=bpphs[1].binarypackagerelease)
1191 make_publications_arch_specific([dependent], True)
1192 self.assertEqual(
1193 ['1.2', '1.1'], find_live_binary_versions_pass_2(bpphs))
1194
1195
1196class TestDominationHelpers(TestCaseWithFactory):
1197 """Test lightweight helpers for the `Dominator`."""
1198
1199 layer = ZopelessDatabaseLayer
1200
1201 def test_contains_arch_indep_says_True_for_arch_indep(self):
1202 bpphs = [self.factory.makeBinaryPackagePublishingHistory()]
1203 make_publications_arch_specific(bpphs, False)
1204 self.assertTrue(contains_arch_indep(bpphs))
1205
1206 def test_contains_arch_indep_says_False_for_arch_specific(self):
1207 bpphs = [self.factory.makeBinaryPackagePublishingHistory()]
1208 make_publications_arch_specific(bpphs, True)
1209 self.assertFalse(contains_arch_indep(bpphs))
1210
1211 def test_contains_arch_indep_says_True_for_combination(self):
1212 bpphs = make_bpphs_for_versions(self.factory, ['1.1', '1.0'])
1213 make_publications_arch_specific(bpphs[:1], True)
1214 make_publications_arch_specific(bpphs[1:], False)
1215 self.assertTrue(contains_arch_indep(bpphs))
1216
1217 def test_contains_arch_indep_says_False_for_empty_list(self):
1218 self.assertFalse(contains_arch_indep([]))