18588974 solver can fail with faulty rejections for incorporates
authorShawn Walker <shawn.walker@oracle.com>
Sat, 12 Apr 2014 23:31:42 -0700
changeset 3078 cef31428bd48
parent 3077 d1bfffec3968
child 3079 e79e515aaf9a
18588974 solver can fail with faulty rejections for incorporates
src/modules/client/pkg_solver.py
src/tests/cli/t_pkg_install.py
--- a/src/modules/client/pkg_solver.py	Mon Apr 28 12:58:40 2014 -0700
+++ b/src/modules/client/pkg_solver.py	Sat Apr 12 23:31:42 2014 -0700
@@ -23,6 +23,7 @@
 #
 # Copyright (c) 2007, 2014, Oracle and/or its affiliates. All rights reserved.
 #
+import operator
 import os
 import time
 
@@ -1102,9 +1103,9 @@
                         pass
 
                 mver = fmri.version
-                # Always use a copy; return value may be cached.
-                all_fmris = self.__get_catalog_fmris(fmri.pkg_name)[:]
-                all_fmris.reverse()
+                # Always use a copy in reverse order (so versions are in
+                # descending order); return value may be cached.
+                all_fmris = self.__get_catalog_fmris(fmri.pkg_name)[::-1]
 
                 # frozensets are used so callers don't inadvertently
                 # update these sets (which may be cached).  Iteration is
@@ -1116,7 +1117,6 @@
                 for i, f in enumerate(all_fmris):
                         if mver == f.version:
                                 last_ver = i
-                                continue
                         elif last_ver is not None:
                                 break
 
@@ -1135,6 +1135,103 @@
                 self.__cache[tp] = (matching, remaining)
                 return self.__cache[tp]
 
+        def __comb_common_noversion(self, fmri, dotrim, constraint,
+            obsolete_ok):
+                """Implements versionless comb logic."""
+
+                all_fmris = self.__get_catalog_fmris(fmri.pkg_name)
+                matching = frozenset((
+                    f
+                    for f in all_fmris
+                    if not dotrim or f not in self.__trim_dict
+                    if obsolete_ok or not self.__fmri_is_obsolete(f)
+                ))
+                remaining = frozenset(set(all_fmris) - matching)
+                return matching, remaining
+
+        def __comb_common_version(self, fmri, dotrim, constraint, obsolete_ok):
+                """Implements versioned comb logic."""
+
+                # If using a version constraint that cares about branch (but not
+                # timestr), the fmris will have to be resorted so that the
+                # version chopping done here works as expected.  This is because
+                # version sort order is release, branch, timestr which is
+                # different than is_successor() order.  However, if the provided
+                # FMRI has a timestamp, doesn't have a branch, or we're applying
+                # a constraint that doesn't care about the branch, then we don't
+                # need to resort.
+                branch_sort = not fmri.version.timestr and \
+                    fmri.version.branch and \
+                    constraint not in (version.CONSTRAINT_NONE,
+                        version.CONSTRAINT_RELEASE,
+                        version.CONSTRAINT_RELEASE_MAJOR,
+                        version.CONSTRAINT_RELEASE_MINOR)
+
+                all_fmris = self.__get_catalog_fmris(fmri.pkg_name)
+                mver = fmri.version
+                if branch_sort:
+                        # The first version of this attempted to perform
+                        # multiple passes to avoid the cost of sorting by
+                        # finding the last entry that matched CONSTRAINT_RELEASE
+                        # and then only resorting the slice of comb_fmris from
+                        # first_ver to last_ver, but that actually ended up
+                        # being slower because multiple passes with is_successor()
+                        # (even over a small portion of comb_fmris) is more
+                        # expensive than simply resorting the entire list.
+                        # Ideally, we'd get the entries from
+                        # __get_catalog_fmris() in this order already which
+                        # would be faster since we'd avoid a second sort.
+                        skey = operator.attrgetter(
+                            'version.branch', 'version.release')
+                        # Always use a copy; return value may be cached.
+                        comb_fmris = sorted(all_fmris, key=skey,
+                            reverse=True)
+                else:
+                        # Always use a copy; return value may be cached.
+                        comb_fmris = all_fmris[::-1]
+
+                # Iteration is performed in descending version order with the
+                # assumption that systems are generally up-to-date so it should
+                # be faster to start at the end and look for the oldest version
+                # that matches.
+                first_ver = None
+                last_ver = None
+                for i, f in enumerate(comb_fmris):
+                        fver = f.version
+                        if ((fver.is_successor(mver, constraint=constraint) or 
+                            fver == mver)):
+                                if first_ver is None:
+                                        first_ver = i
+                                last_ver = i
+                        elif last_ver is not None:
+                                break
+
+                if last_ver is not None:
+                        # Oddly enough, it's a little bit faster to iterate
+                        # through the slice of comb_fmris again and store
+                        # matches here instead of above.  Perhaps variable
+                        # scoping overhead is to blame?
+                        matching = []
+                        remaining = []
+                        for f in comb_fmris[first_ver:last_ver + 1]:
+                                if ((not dotrim or
+                                    f not in self.__trim_dict) and
+                                    (obsolete_ok or not
+                                        self.__fmri_is_obsolete(f))):
+                                        matching.append(f)
+                                else:
+                                        remaining.append(f)
+                        matching = frozenset(matching)
+                        remaining = frozenset(chain(
+                            comb_fmris[:first_ver],
+                            remaining,
+                            comb_fmris[last_ver + 1:]))
+                else:
+                        matching = frozenset()
+                        remaining = frozenset(comb_fmris)
+
+                return matching, remaining
+
         def __comb_common(self, fmri, dotrim, constraint, obsolete_ok):
                 """Underlying impl. of other comb routines"""
 
@@ -1145,62 +1242,14 @@
                 if (not self.__trimdone and dotrim) or tp not in self.__cache:
                         # use frozensets so callers don't inadvertently update
                         # these sets (which may be cached).
-                        all_fmris = self.__get_catalog_fmris(fmri.pkg_name)
-                        mver = fmri.version
-                        if not mver:
-                                matching = frozenset((
-                                    f
-                                    for f in all_fmris
-                                    if not dotrim or f not in self.__trim_dict
-                                    if obsolete_ok or not self.__fmri_is_obsolete(f)
-                                ))
-                                remaining = frozenset(set(all_fmris) - matching)
+                        if not fmri.version or not fmri.version.release:
+                                matching, remaining = \
+                                    self.__comb_common_noversion(fmri, dotrim,
+                                        constraint, obsolete_ok)
                         else:
-                                # Always use a copy; return value may be cached.
-                                all_fmris = all_fmris[:]
-                                all_fmris.reverse()
-
-                                # Iteration is performed in descending version
-                                # order with the assumption that systems are
-                                # generally up-to-date so it should be faster to
-                                # start at the end and look for the oldest
-                                # version that matches.
-                                first_ver = None
-                                last_ver = None
-                                for i, f in enumerate(all_fmris):
-                                        fver = f.version
-                                        if (fver.is_successor(mver,
-                                            constraint=constraint) or \
-                                                fver == mver):
-                                                if first_ver is None:
-                                                        first_ver = i
-                                                last_ver = i
-                                        elif last_ver is not None:
-                                                break
-
-                                if last_ver is not None:
-                                        # Oddly enough, it's a little bit faster
-                                        # to iterate through the slice of
-                                        # all_fmris again and store matches here
-                                        # instead of above.  Perhaps variable
-                                        # scoping overhead is to blame?
-                                        matching = []
-                                        remaining = []
-                                        for f in all_fmris[first_ver:last_ver + 1]:
-                                                if ((not dotrim or
-                                                    f not in self.__trim_dict) and
-                                                    (obsolete_ok or not
-                                                        self.__fmri_is_obsolete(f))):
-                                                        matching.append(f)
-                                                else:
-                                                        remaining.append(f)
-                                        matching = frozenset(matching)
-                                        remaining = frozenset(chain(remaining,
-                                            all_fmris[:first_ver],
-                                            all_fmris[last_ver + 1:]))
-                                else:
-                                        matching = frozenset()
-                                        remaining = frozenset(all_fmris)
+                                matching, remaining = \
+                                    self.__comb_common_version(fmri, dotrim,
+                                        constraint, obsolete_ok)
 
                         # if we haven't finished trimming, don't cache this
                         if not self.__trimdone:
--- a/src/tests/cli/t_pkg_install.py	Mon Apr 28 12:58:40 2014 -0700
+++ b/src/tests/cli/t_pkg_install.py	Sat Apr 12 23:31:42 2014 -0700
@@ -7648,6 +7648,7 @@
                 self.pkg("update -v")
                 self.pkg("list inc2p2", exit=1)
 
+
 class TestObsoletionNestedIncorporations(pkg5unittest.SingleDepotTestCase):
         # Only start/stop the depot once (instead of for every test)
 
@@ -7779,6 +7780,76 @@
                 self.pkg("%s stem" % install_cmd, exit=1)
 
 
+class TestPkgInstallMultiIncorp(pkg5unittest.ManyDepotTestCase):
+        """Tests involving incorporations and multiple publishers."""
+
+        incorporated_latest = """
+            open [email protected]
+            close
+            open [email protected]
+            close"""
+
+        incorporated = """
+            open [email protected]
+            close
+            open [email protected]
+            close """
+
+        incorporates = """
+            open [email protected]
+            add depend type=incorporate [email protected]
+            close
+            open [email protected]
+            add depend type=incorporate [email protected]
+            close"""
+
+        persistent_setup = True
+
+        def setUp(self):
+                pkg5unittest.ManyDepotTestCase.setUp(self, ["test1", "test2"])
+                self.rurl1 = self.dcs[1].get_repo_url()
+                self.rurl2 = self.dcs[2].get_repo_url()
+
+        def test_1_incorp_latest_older(self):
+                """Ensure that if the newest release version of a package is
+                available for an older branch that incorporate dependencies work
+                as expected."""
+
+                self.image_create(self.rurl1)
+                self.pkgsend_bulk(self.rurl1, (self.incorporates,
+                    self.incorporated, self.incorporated_latest))
+
+                # First, install two incorporations that intersect such that
+                # only the version before the latest branch can be installed.
+                self.pkg("install userland-incorporation vim-incorporation")
+
+                # Then, attempt to install vim; this should succeed even though
+                # the newest version available is for an older branch.
+                self.pkg("install [email protected]")
+
+        def test_2_incorp_multi_pub(self):
+                """Ensure that if another publisher offers newer packages that
+                satisfy an incorporate dependency, but are rejected because of
+                publisher selection, that the preferred publisher's package can
+                still satisfy the incorporate."""
+
+                self.image_create(self.rurl1)
+                self.pkgsend_bulk(self.rurl1, (self.incorporates,
+                    self.incorporated))
+                self.pkgsend_bulk(self.rurl2, self.incorporated_latest)
+
+                # First, install the incorporation.
+                self.pkg("install userland-incorporation")
+
+                # Next, add the second publisher.
+                self.pkg("set-publisher -p %s" % self.rurl2)
+
+                # Next, verify that first publisher's incorporated package can
+                # be installed since it satisfies incorporate dependencies even
+                # though second publisher's versions will be rejected.
+                self.pkg("install //test1/vim")
+
+
 class TestPkgInstallLicense(pkg5unittest.SingleDepotTestCase):
         """Tests involving one or more packages that require license acceptance
         or display."""