← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/pas-arm into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/pas-arm into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #917708 in Launchpad itself: "Launchpad does not recognize Arch = any-arm"
  https://bugs.launchpad.net/launchpad/+bug/917708

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/pas-arm/+merge/107966

== Summary ==

Bug 917708 reports that the "any-arm" wildcard is not recognised in packages' Architecture fields.

== Proposed fix ==

Use dpkg-architecture to match architecture wildcards rather than doing an incomplete NIH job.

== Implementation details ==

Launchpad does its own parsing of architecture wildcards, but it does an incomplete job; it has misunderstood the second field of an any-* wildcard as being like an architecture name, whereas actually it's a canonicalised CPU name: these are almost equivalent, but not quite.  The problem reported here is a perfect example of where they aren't equivalent.

Doing a more complete job of this in the framework of Launchpad's hand-rolled parser would involve either (a) a grotty accretion of special cases or (b) parsing dpkg-internal files such as /usr/share/dpkg/triplettable and /usr/share/dpkg/cputable.  Really, though, it should delegate this task to the packaging system, which knows how to do it.  Unfortunately the only public interfaces to this are by way of dpkg-architecture subprocess execution or a Perl module; but the product of the architectures supported by Launchpad and the set of wildcards in use is never going to be particularly large, so a reasonably economical way to approach this is just to cache the result of calling dpkg-architecture for every (architecture, wildcard) pair.

To restore the balance of the LoC force, I disassembled package-arch-specific.txt and merged it into test_pas.py.  doctests--

== Tests ==

bin/test -vvct test_pas

== Demo and Q/A ==

I'm not sure it's possible to demo the fix as such, since there are no ARM builders on dogfood.  To avoid trivial embarrassment, though, it might be worth uploading an any-i386 package to dogfood and making sure that that still gets scheduled for building on i386.
-- 
https://code.launchpad.net/~cjwatson/launchpad/pas-arm/+merge/107966
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/pas-arm into lp:launchpad.
=== removed file 'lib/lp/soyuz/doc/package-arch-specific.txt'
--- lib/lp/soyuz/doc/package-arch-specific.txt	2012-01-20 15:42:44 +0000
+++ lib/lp/soyuz/doc/package-arch-specific.txt	1970-01-01 00:00:00 +0000
@@ -1,271 +0,0 @@
-= Package Architecture Specific =
-
-The Package-Architecture-Specific (PAS) module is a parser to a set
-of restrictions to build packages based on archive-admin experiences.
-
-Those restrictions overlap the metadata information from the source
-packages (architecturehintlist).
-
-For testing PAS we will create a set of publications using
-`SoyuzTestPublisher`.
-
-    >>> from lp.registry.interfaces.distribution import (
-    ...     IDistributionSet)
-    >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
-    >>> hoary = ubuntu.getSeries('hoary')
-
-    >>> from lp.soyuz.tests.test_publishing import (
-    ...     SoyuzTestPublisher)
-    >>> from lp.testing.dbuser import (
-    ...     lp_dbuser,
-    ...     switch_dbuser,
-    ...     )
-
-Publication will be added as 'launchpad' DB user.
-
-    >>> switch_dbuser("launchpad")
-
-    >>> test_publisher = SoyuzTestPublisher()
-    >>> ignore = test_publisher.setUpDefaultDistroSeries(hoary)
-    >>> test_publisher.addFakeChroots()
-
-    >>> pub_three = test_publisher.getPubSource(
-    ...     sourcename='test-buildd-3', version='669',
-    ...     architecturehintlist="i386 hppa amd64")
-
-Create a PPA source publication.
-
-    >>> from lp.registry.interfaces.person import IPersonSet
-    >>> cprov = getUtility(IPersonSet).getByName('cprov')
-
-    >>> pub_ppa = test_publisher.getPubSource(
-    ...     sourcename='test-ppa', version='675',
-    ...     architecturehintlist="i386 hppa",
-    ...     archive=cprov.archive)
-
-Good, all done, we can commit the publications and continue the tests
-with the buildmaster DB user.
-
-    >>> switch_dbuser(test_dbuser)
-
-
-== Check the architectures to build ==
-
-Hoary has only 'hppa' and 'i386' architectures.
-
-    >>> legal_archs = set(hoary.architectures)
-
-    >>> def print_architectures(architectures):
-    ...     arch_tags = [arch.architecturetag for arch in architectures]
-    ...     for tag in sorted(arch_tags):
-    ...         print tag
-
-    >>> print_architectures(legal_archs)
-    hppa
-    i386
-
-See if the code which determines archs to build does the right thing for
-each of these options:
-
-    >>> from lp.soyuz.pas import (
-    ...     BuildDaemonPackagesArchSpecific, determineArchitecturesToBuild)
-
-    >>> def print_build_architectures(pub, pas_verify=None):
-    ...     allowed_architectures = determineArchitecturesToBuild(
-    ...         pub, legal_archs, hoary, pas_verify)
-    ...     print_architectures(allowed_architectures)
-
-=== Source PAS ===
-
-Source PAS lines are the ones starting with '%' followed by the source
-package name, colon and then a list of architecture tags separated by
-space.
-
-Create a harness that lets us easily test PAS statements:
-
-    >>> import os
-    >>> import tempfile
-    >>> import shutil
-
-    >>> def getPASVerifier(pas_string):
-    ...   """Build and return a PAS verifier based on the string provided."""
-    ...   temp_dir = tempfile.mkdtemp()
-    ...   pas_filename = os.path.join(temp_dir, "Packages-arch-specific")
-    ...   pas_file = open(pas_filename, "w")
-    ...   pas_file.write(pas_string)
-    ...   pas_file.close()
-    ...   pas_verify = BuildDaemonPackagesArchSpecific(temp_dir, hoary)
-    ...   shutil.rmtree(temp_dir)
-    ...   return pas_verify
-
-Normally, 'pub_three' would be built on all available architectures in
-hoary.
-
-    >>> print_build_architectures(pub_three)
-    hppa
-    i386
-
-But it it can be 'temporarily' restricted to build only on 'i386' via
-PAS.
-
-    >>> pas_verify = getPASVerifier("%test-buildd-3: i386")
-    >>> print_build_architectures(pub_three, pas_verify)
-    i386
-
-Or even restricted to not be built in any architecture.
-
-    >>> pas_verify = getPASVerifier("%test-buildd-3: sparc")
-    >>> print_build_architectures(pub_three, pas_verify)
-
-PAS can also be used for excluding specific architectures.
-
-    >>> pas_verify = getPASVerifier("%test-buildd-3: !i386")
-    >>> print_build_architectures(pub_three, pas_verify)
-    hppa
-
-PPA builds are not affected by PAS restrictions, i.e., they will
-build for all requested architectures currently supported in the PPA
-subsystem.
-
-    >>> pas_verify = getPASVerifier("%test-ppa: hppa")
-    >>> print_build_architectures(pub_ppa, pas_verify)
-    i386
-
-    >>> pas_verify = getPASVerifier("%test-ppa: !i386")
-    >>> print_build_architectures(pub_ppa, pas_verify)
-    i386
-
-
-== Binary PAS ==
-
-Binary PAS lines are the ones which does not start with '%'. They
-contain binary package name, colon and then a list of architecture
-tags separated by space.
-
-Binary PAS lines work as a shortcut to the source when they produce
-only a single binary. When the binary mentioned is part of a
-collection of binaries produced by the same source the architecture
-restriction does not make sense anymore. See more information about
-this aspect below.
-
-To check binary PAS listings we'll use a source publication for which
-produces a single binary.
-
-    >>> from lp.soyuz.enums import (
-    ...     PackagePublishingStatus)
-
-    >>> with lp_dbuser():
-    ...     pub_single = test_publisher.getPubSource(
-    ...         sourcename='single', version='1.0',
-    ...         architecturehintlist="any")
-    ...     binaries = test_publisher.getPubBinaries(
-    ...         binaryname='single-bin', pub_source=pub_single,
-    ...         status=PackagePublishingStatus.PUBLISHED)
-
-    >>> len(set(pub.binarypackagerelease.name
-    ...         for pub in pub_single.getPublishedBinaries()))
-    1
-
-Normally the source would be built in all the available architectures
-in hoary.
-
-    >>> print_build_architectures(pub_single)
-    hppa
-    i386
-
-The source building does not get affect by an unrelated binary PAS line.
-
-    >>> pas_verify = getPASVerifier("boing: i386")
-    >>> print_build_architectures(pub_single, pas_verify)
-    hppa
-    i386
-
-Using PAS, we can restrict the building architectures by tagging the
-produced binary with a specific list of allowed architectures.
-
-    >>> pas_verify = getPASVerifier("single-bin: i386 sparc")
-    >>> print_build_architectures(pub_single, pas_verify)
-    i386
-
-It's also possible to only block specific architectures.
-
-    >>> pas_verify = getPASVerifier("single-bin: !hppa")
-    >>> print_build_architectures(pub_single, pas_verify)
-    i386
-
-Although it's not possible to block the build on the
-'nominatedarchindep' architecture via a binary PAS line. The reason
-for that is the fact the architecture independent binaries are only
-built in i386, if it gets blacklisted those binaries won't be
-built. This would never be a legitimate use-case.
-
-    >>> pas_verify = getPASVerifier("single-bin: !i386 !hppa")
-    >>> print_build_architectures(pub_single, pas_verify)
-    i386
-
-Binary PAS lines are ignored for PPAs as well. We create a binary for
-the existing test PPA source publication.
-
-    >>> with lp_dbuser():
-    ...     pub_ppa.archive.require_virtualized = False
-    ...     binaries = test_publisher.getPubBinaries(
-    ...         binaryname='ppa-bin', pub_source=pub_ppa,
-    ...         status=PackagePublishingStatus.PUBLISHED)
-
-And it will build in the same architectures with or without a
-corresponding PAS binary line.
-
-    >>> print_build_architectures(pub_ppa)
-    hppa
-    i386
-
-    >>> pas_verify = getPASVerifier("ppa-bin: !hppa")
-    >>> print_build_architectures(pub_single, pas_verify)
-    hppa
-    i386
-
-As mentioned above, PAS binary lines referring to binary packages for
-which the source produce other binaries are completely ignored. Other
-tools use that information but we can't restrict builds in this
-circumstances.
-
-We will create a 'multiple' source publication build two binaries,
-'bin-one' and 'bin-two'.
-
-    >>> with lp_dbuser():
-    ...     pub_multiple = test_publisher.getPubSource(
-    ...         sourcename='multiple', version='1.1',
-    ...         architecturehintlist="any")
-    ...     for build in pub_multiple.createMissingBuilds():
-    ...         bin_one = test_publisher.uploadBinaryForBuild(
-    ...             build, 'bin-one')
-    ...         pub_bin_one = test_publisher.publishBinaryInArchive(
-    ...             bin_one, pub_multiple.archive,
-    ...             status=PackagePublishingStatus.PUBLISHED)
-    ...         bin_two = test_publisher.uploadBinaryForBuild(
-    ...             build, 'bin-two')
-    ...         pub_bin_two = test_publisher.publishBinaryInArchive(
-    ...             bin_two, pub_multiple.archive,
-    ...             status=PackagePublishingStatus.PUBLISHED)
-
-    >>> len(set(pub.binarypackagerelease.name
-    ...         for pub in pub_multiple.getPublishedBinaries()))
-    2
-
-Any attempt to restrict the building architectures on 'multiple'
-source via its binaries will be ignored and it will continue to be
-built for all available architectures.
-
-    >>> print_build_architectures(pub_multiple)
-    hppa
-    i386
-
-    >>> pas_verify = getPASVerifier("bin-one: i386 sparc")
-    >>> print_build_architectures(pub_multiple, pas_verify)
-    hppa
-    i386
-
-    >>> pas_verify = getPASVerifier("bin-two: !hppa")
-    >>> print_build_architectures(pub_multiple, pas_verify)
-    hppa
-    i386

=== modified file 'lib/lp/soyuz/pas.py'
--- lib/lp/soyuz/pas.py	2011-01-13 00:45:18 +0000
+++ lib/lp/soyuz/pas.py	2012-05-30 12:52:24 +0000
@@ -1,8 +1,9 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 import operator
 import os
+import subprocess
 
 from sqlobject import SQLObjectNotFound
 
@@ -109,6 +110,29 @@
         return source_name, arch_tags
 
 
+class DpkgArchitectureCache:
+    """Cache the results of asking questions of dpkg-architecture."""
+
+    def __init__(self):
+        self._matches = {}
+
+    def match(self, arch, wildcard):
+        if (arch, wildcard) not in self._matches:
+            command = ["dpkg-architecture", "-i%s" % wildcard]
+            env = dict(os.environ)
+            env["DEB_HOST_ARCH"] = arch
+            ret = (subprocess.call(command, env=env) == 0)
+            self._matches[(arch, wildcard)] = ret
+        return self._matches[(arch, wildcard)]
+
+    def findAllMatches(self, arches, wildcards):
+        return [arch for arch in arches for wildcard in wildcards
+                if self.match(arch, wildcard)]
+
+
+dpkg_architecture = DpkgArchitectureCache()
+
+
 def determineArchitecturesToBuild(pubrec, legal_archseries,
                                   distroseries, pas_verify=None):
     """Return a list of architectures for which this publication should build.
@@ -168,29 +192,17 @@
         arch.architecturetag for arch in legal_archseries if arch.enabled)
 
     hint_archs = set(hint_string.split())
-
-    # If a *-any architecture wildcard is present, build for everything
-    # we can. We only support Linux-based architectures at the moment,
-    # and any-any isn't a valid wildcard. See bug #605002.
-    if hint_archs.intersection(('any', 'linux-any')):
-        package_tags = legal_arch_tags
-    else:
-        # We need to support arch tags like any-foo and linux-foo, so remove
-        # supported kernel prefixes. See bug #73761.
-        stripped_archs = hint_archs
-        for kernel in ('linux', 'any'):
-            stripped_archs = set(
-                arch.replace("%s-" % kernel, "") for arch in stripped_archs)
-        package_tags = stripped_archs.intersection(legal_arch_tags)
-
-        # 'all' is only used as a last resort, to create an arch-indep
-        # build where no builds would otherwise exist.
-        if len(package_tags) == 0 and 'all' in hint_archs:
-            nominated_arch = distroseries.nominatedarchindep
-            if nominated_arch in legal_archseries:
-                package_tags = set([nominated_arch.architecturetag])
-            else:
-                package_tags = set()
+    package_tags = set(dpkg_architecture.findAllMatches(
+        legal_arch_tags, hint_archs))
+
+    # 'all' is only used as a last resort, to create an arch-indep build
+    # where no builds would otherwise exist.
+    if len(package_tags) == 0 and 'all' in hint_archs:
+        nominated_arch = distroseries.nominatedarchindep
+        if nominated_arch in legal_archseries:
+            package_tags = set([nominated_arch.architecturetag])
+        else:
+            package_tags = set()
 
     if pas_verify:
         build_tags = set()

=== modified file 'lib/lp/soyuz/tests/test_doc.py'
--- lib/lp/soyuz/tests/test_doc.py	2012-05-25 13:26:08 +0000
+++ lib/lp/soyuz/tests/test_doc.py	2012-05-30 12:52:24 +0000
@@ -157,11 +157,6 @@
         setUp=manageChrootSetup,
         layer=LaunchpadZopelessLayer,
         ),
-    'package-arch-specific.txt': LayeredDocFileSuite(
-        '../doc/package-arch-specific.txt',
-        setUp=builddmasterSetUp,
-        layer=LaunchpadZopelessLayer,
-        ),
     'queuebuilder.txt': LayeredDocFileSuite(
         '../doc/queuebuilder.txt',
         setUp=builddmasterSetUp,

=== modified file 'lib/lp/soyuz/tests/test_pas.py'
--- lib/lp/soyuz/tests/test_pas.py	2012-01-01 02:58:52 +0000
+++ lib/lp/soyuz/tests/test_pas.py	2012-05-30 12:52:24 +0000
@@ -1,7 +1,17 @@
-# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-from lp.soyuz.pas import determineArchitecturesToBuild
+import os
+
+from lp.soyuz.enums import (
+    ArchivePurpose,
+    PackagePublishingStatus,
+    )
+from lp.soyuz.model.processor import ProcessorFamily
+from lp.soyuz.pas import (
+    BuildDaemonPackagesArchSpecific,
+    determineArchitecturesToBuild,
+    )
 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
 from lp.testing import TestCaseWithFactory
 from lp.testing.layers import LaunchpadZopelessLayer
@@ -16,23 +26,51 @@
         super(TestDetermineArchitecturesToBuild, self).setUp()
         self.publisher = SoyuzTestPublisher()
         self.publisher.prepareBreezyAutotest()
+        armel_family = ProcessorFamily.get(5)
+        if not armel_family.processors:
+            armel_family.addProcessor('armel', 'armel', 'armel')
+        self.publisher.breezy_autotest.newArch(
+            'armel', armel_family, False, self.publisher.person)
         self.publisher.addFakeChroots()
 
-    def assertArchsForHint(self, hint_string, expected_arch_tags,
-                           allowed_arch_tags=None):
-        """Assert that the given hint resolves to the expected archtags."""
-        pub = self.publisher.getPubSource(architecturehintlist=hint_string)
+    def getPASVerifier(self, pas_string):
+        """Build and return a PAS verifier based on the string provided."""
+        temp_dir = self.makeTemporaryDirectory()
+        pas_filename = os.path.join(temp_dir, "Packages-arch-specific")
+        with open(pas_filename, "w") as pas_file:
+            pas_file.write(pas_string)
+        pas_verify = BuildDaemonPackagesArchSpecific(
+            temp_dir, self.publisher.breezy_autotest)
+        return pas_verify
+
+    def assertArchitecturesToBuild(self, expected_arch_tags, pub,
+                                   allowed_arch_tags=None, pas_string=None):
         if allowed_arch_tags is None:
             allowed_archs = self.publisher.breezy_autotest.architectures
         else:
             allowed_archs = [
                 arch for arch in self.publisher.breezy_autotest.architectures
                 if arch.architecturetag in allowed_arch_tags]
+        if pas_string is None:
+            pas_verify = None
+        else:
+            pas_verify = self.getPASVerifier(pas_string)
         architectures = determineArchitecturesToBuild(
-            pub, allowed_archs, self.publisher.breezy_autotest)
-        self.assertEqual(
+            pub, allowed_archs, self.publisher.breezy_autotest,
+            pas_verify=pas_verify)
+        self.assertContentEqual(
             expected_arch_tags, [a.architecturetag for a in architectures])
 
+    def assertArchsForHint(self, hint_string, expected_arch_tags,
+                           allowed_arch_tags=None, sourcename=None,
+                           pas_string=None):
+        """Assert that the given hint resolves to the expected archtags."""
+        pub = self.publisher.getPubSource(
+            sourcename=sourcename, architecturehintlist=hint_string)
+        self.assertArchitecturesToBuild(
+            expected_arch_tags, pub, allowed_arch_tags=allowed_arch_tags,
+            pas_string=pas_string)
+
     def test_single_architecture(self):
         # A hint string with a single arch resolves to just that arch.
         self.assertArchsForHint('hppa', ['hppa'])
@@ -58,7 +96,7 @@
 
     def test_wildcard(self):
         # 'any' is a wildcard that matches all available archs.
-        self.assertArchsForHint('any', ['hppa', 'i386'])
+        self.assertArchsForHint('any', ['armel', 'hppa', 'i386'])
 
     def test_kernel_specific_architecture(self):
         # Since we only support Linux-based architectures, 'linux-foo'
@@ -73,9 +111,14 @@
         # Wildcards work for kernels: 'any-foo' is treated like 'foo'.
         self.assertArchsForHint('any-hppa', ['hppa'])
 
+    def test_kernel_wildcard_architecture_arm(self):
+        # The second part of a wildcard matches the canonical CPU name, not
+        # on the Debian architecture, so 'any-arm' matches 'armel'.
+        self.assertArchsForHint('any-arm', ['armel'])
+
     def test_kernel_specific_architecture_wildcard(self):
         # Wildcards work for archs too: 'linux-any' is treated like 'any'.
-        self.assertArchsForHint('linux-any', ['hppa', 'i386'])
+        self.assertArchsForHint('linux-any', ['armel', 'hppa', 'i386'])
 
     def test_unknown_kernel_specific_architecture_wildcard(self):
         # But unknown kernels continue to result in nothing.
@@ -83,20 +126,21 @@
 
     def test_wildcard_and_independent(self):
         # 'all' continues to be ignored alongside a valid wildcard.
-        self.assertArchsForHint('all linux-any', ['hppa', 'i386'])
+        self.assertArchsForHint('all linux-any', ['armel', 'hppa', 'i386'])
 
     def test_kernel_independent_is_invalid(self):
         # 'linux-all' isn't supported.
         self.assertArchsForHint('linux-all', [])
 
-    def test_double_wildcard_is_invalid(self):
-        # 'any-any' is invalid; you want 'any'.
-        self.assertArchsForHint('any-any', [])
+    def test_double_wildcard_is_same_as_single(self):
+        # 'any-any' is redundant with 'any', but dpkg-architecture supports
+        # it anyway.
+        self.assertArchsForHint('any-any', ['armel', 'hppa', 'i386'])
 
     def test_disabled_architectures_omitted(self):
         # Disabled architectures are not buildable, so are excluded.
         self.publisher.breezy_autotest['hppa'].enabled = False
-        self.assertArchsForHint('any', ['i386'])
+        self.assertArchsForHint('any', ['armel', 'i386'])
 
     def test_virtualized_archives_have_only_virtualized_archs(self):
         # For archives which must build on virtual builders, only
@@ -110,3 +154,128 @@
         # i386) is omitted, no builds will be created for arch-indep
         # sources.
         self.assertArchsForHint('all', [], allowed_arch_tags=['hppa'])
+
+    def test_source_pas_defaults_to_all_available_architectures(self):
+        # Normally, a source package will be built on all available
+        # architectures in the series.
+        self.assertArchsForHint(
+            "i386 hppa amd64", ["hppa", "i386"], pas_string="")
+
+    def test_source_pas_can_restrict_to_one_architecture(self):
+        # A source package can be restricted to a single architecture via PAS.
+        self.assertArchsForHint(
+            "i386 hppa amd64", ["i386"], sourcename="test",
+            pas_string="%test: i386")
+
+    def test_source_pas_can_restrict_to_no_architectures(self):
+        # A source package can be restricted to not built on any architecture.
+        self.assertArchsForHint(
+            "i386 hppa amd64", [], sourcename="test",
+            pas_string="%test: sparc")
+
+    def test_source_pas_can_exclude_specific_architecture(self):
+        # A source PAS entry can exclude a specific architecture.
+        self.assertArchsForHint(
+            "i386 hppa amd64", ["hppa"], sourcename="test",
+            pas_string="%test: !i386")
+
+    def setUpPPAAndSource(self):
+        # Create a PPA and return a new source publication in it.
+        archive = self.factory.makeArchive(
+            distribution=self.publisher.ubuntutest, purpose=ArchivePurpose.PPA)
+        return self.publisher.getPubSource(
+            sourcename="test-ppa", architecturehintlist="i386 hppa",
+            archive=archive)
+
+    def test_source_pas_does_not_affect_ppa(self):
+        # PPA builds are not affected by source PAS restrictions; that is,
+        # they will build for all requested architectures currently
+        # supported in the PPA subsystem.
+        pub_ppa = self.setUpPPAAndSource()
+        self.assertArchitecturesToBuild(
+            ["i386"], pub_ppa, pas_string="%test-ppa: hppa")
+        self.assertArchitecturesToBuild(
+            ["i386"], pub_ppa, pas_string="%test-ppa: !i386")
+
+    def setUpSourceAndBinary(self):
+        # To check binary PAS listings we'll use a source publication which
+        # produces a single binary.
+        pub_single = self.publisher.getPubSource(
+            sourcename="single", architecturehintlist="any")
+        binaries = self.publisher.getPubBinaries(
+            binaryname="single-bin", pub_source=pub_single,
+            status=PackagePublishingStatus.PUBLISHED)
+        binary_names = set(
+            pub.binarypackagerelease.name
+            for pub in pub_single.getPublishedBinaries())
+        self.assertEqual(1, len(binary_names))
+        return pub_single, binaries
+
+    def test_binary_pas_unrelated_binary_lines_have_no_effect(self):
+        # Source packages are unaffected by an unrelated binary PAS line.
+        pub_single, binaries = self.setUpSourceAndBinary()
+        self.assertArchitecturesToBuild(
+            ["armel", "hppa", "i386"], pub_single, pas_string="boing: i386")
+
+    def test_binary_pas_can_restrict_architectures(self):
+        # A PAS entry can restrict the build architectures by tagging the
+        # produced binary with a list of allowed architectures.
+        pub_single, binaries = self.setUpSourceAndBinary()
+        self.assertArchitecturesToBuild(
+            ["i386"], pub_single, pas_string="single-bin: i386 sparc")
+
+    def test_binary_pas_can_exclude_specific_architecture(self):
+        # A binary PAS entry can exclude a specific architecture.
+        pub_single, binaries = self.setUpSourceAndBinary()
+        self.assertArchitecturesToBuild(
+            ["armel", "i386"], pub_single, pas_string="single-bin: !hppa")
+
+    def test_binary_pas_cannot_exclude_nominatedarchindep(self):
+        # A binary PAS entry cannot exclude the 'nominatedarchindep'
+        # architecture.  Architecture-independent binaries are only built on
+        # nominatedarchindep; if that architecture is blacklisted, those
+        # binaries will never be built.
+        pub_single, binaries = self.setUpSourceAndBinary()
+        self.assertArchitecturesToBuild(
+            ["i386"], pub_single, pas_string="single-bin: !i386 !hppa !armel")
+
+    def test_binary_pas_does_not_affect_ppa(self):
+        # PPA builds are not affected by binary PAS restrictions.
+        pub_ppa = self.setUpPPAAndSource()
+        pub_ppa.archive.require_virtualized = False
+        self.publisher.getPubBinaries(
+            binaryname="ppa-bin", pub_source=pub_ppa,
+            status=PackagePublishingStatus.PUBLISHED)
+        self.assertArchitecturesToBuild(
+            ["hppa", "i386"], pub_ppa, pas_string="")
+        self.assertArchitecturesToBuild(
+            ["hppa", "i386"], pub_ppa, pas_string="ppa-bin: !hppa")
+
+    def test_binary_pas_does_not_affect_multi_binary_sources(self):
+        # Binary PAS entries referring to binary packages whose source
+        # produces other binaries are completely ignored.  Other tools use
+        # that information, but we can't restrict builds in this
+        # circumstance.
+        pub_multiple = self.publisher.getPubSource(
+            sourcename="multiple", architecturehintlist="any")
+        for build in pub_multiple.createMissingBuilds():
+            bin_one = self.publisher.uploadBinaryForBuild(build, "bin-one")
+            self.publisher.publishBinaryInArchive(
+                bin_one, pub_multiple.archive,
+                status=PackagePublishingStatus.PUBLISHED)
+            bin_two = self.publisher.uploadBinaryForBuild(build, "bin-two")
+            self.publisher.publishBinaryInArchive(
+                bin_two, pub_multiple.archive,
+                status=PackagePublishingStatus.PUBLISHED)
+        binary_names = set(
+            pub.binarypackagerelease.name
+            for pub in pub_multiple.getPublishedBinaries())
+        self.assertEqual(2, len(binary_names))
+        self.assertArchitecturesToBuild(
+            ["armel", "hppa", "i386"], pub_multiple, pas_string="")
+        self.assertArchitecturesToBuild(
+            ["armel", "hppa", "i386"], pub_multiple,
+            pas_string="bin-one: i386 sparc")
+        self.assertArchitecturesToBuild(
+            ["armel", "hppa", "i386"], pub_multiple,
+            pas_string="bin-two: !hppa")


Follow ups