eclipseo / rpms / dnf

Forked from rpms/dnf 5 years ago
Clone

Blame 0001-Restore-strict-choice-for-group-installs-1461539.patch

1a4e2c4
From edbd86922b2733fd36622abadca15e744d12bfde Mon Sep 17 00:00:00 2001
1a4e2c4
From: Adam Williamson <awilliam@redhat.com>
1a4e2c4
Date: Tue, 13 Mar 2018 15:33:24 -0700
1a4e2c4
Subject: [PATCH] Restore 'strict' choice for group installs (#1461539)
1a4e2c4
1a4e2c4
This makes the `strict` parameter to `group_install` mean
1a4e2c4
something again.
1a4e2c4
1a4e2c4
When it is set `True`, DNF will behave as yum previously did,
1a4e2c4
which has been a request for several years now. See:
1a4e2c4
1a4e2c4
https://bugzilla.redhat.com/show_bug.cgi?id=1292892
1a4e2c4
https://bugzilla.redhat.com/show_bug.cgi?id=1337731
1a4e2c4
https://bugzilla.redhat.com/show_bug.cgi?id=1427365
1a4e2c4
https://bugzilla.redhat.com/show_bug.cgi?id=1461539
1a4e2c4
1a4e2c4
And commits:
1a4e2c4
1a4e2c4
91f9ce98dbb630800017f44180d636af395435cd
1a4e2c4
1e1901822748b754d038dd396655c997281a7201
1a4e2c4
1a4e2c4
We have been around the houses on this multiple times. @mikem23
1a4e2c4
actually got things right way way back in #1292892:
1a4e2c4
1a4e2c4
"yum-deprecated behaves more sanely here.
1a4e2c4
1a4e2c4
In case 1 [package exists but has dependency issues], it reports
1a4e2c4
the missing dependency and exits with an error
1a4e2c4
1a4e2c4
...
1a4e2c4
1a4e2c4
In case 2 [package cannot be found at all], it exits cleanly, but
1a4e2c4
prints a warning."
1a4e2c4
1a4e2c4
Note that this applies to yum's *default* behaviour: it had an
1a4e2c4
option, --skip-broken, which caused it to skip packages with
1a4e2c4
dependency errors.
1a4e2c4
1a4e2c4
That's exactly what we've wanted all along, and it's also what
1a4e2c4
1461539 requests. Unfortunately, the initial commit intended to
1a4e2c4
fix #1292892 - that's 91f9ce9 - did not quite behave as Mike
1a4e2c4
requested. It treated any 'mandatory' package having dependency
1a4e2c4
errors *or* not existing at all as fatal errors, while not
1a4e2c4
treating non-existence *or* errors on the part of a 'default'
1a4e2c4
or 'optional' package as a fatal error. This introduced *two*
1a4e2c4
differences from yum's behaviour (making the comps 'type' matter,
1a4e2c4
which it never did to yum, and failing on non-existence), which
1a4e2c4
I think was the source of quite a lot of confusion.
1a4e2c4
1a4e2c4
We then wound up filing bugs on this - like #1337731 - and the
1a4e2c4
response to that was 1e19018, which causes dnf to treat *neither*
1a4e2c4
case as a fatal error for *any* package, which is where we stand
1a4e2c4
at present.
1a4e2c4
1a4e2c4
When resolving a transaction, currently, if it cannot find a
1a4e2c4
package corresponding to an entry in a group, dnf will log a
1a4e2c4
warning and carry on. This is good and fine and just what we want
1a4e2c4
it to do. However, when a package is available but cannot be
1a4e2c4
installed, it does the wrong thing. The various forks of this
1a4e2c4
`trans_install` internal function all wind up passing
1a4e2c4
`optional=True` to the Goal, which tells it that it's fine to
1a4e2c4
just leave the package out if its dependencies can't be resolved.
1a4e2c4
Additionally, if this happens, `resolve()` itself does not log
1a4e2c4
anything.
1a4e2c4
1a4e2c4
If you try a group install through the CLI, it will tell you
1a4e2c4
about packages that have been left out due to dependency errors,
1a4e2c4
via the `_skipped_packages` function in `dnf/output.py` which
1a4e2c4
actually sucks them out of the Goal instance itself. But that's
1a4e2c4
no use to anything besides the CLI. Other things which use
1a4e2c4
`resolve()`, like anaconda, have no means of accessing this
1a4e2c4
information, so when this happens to them, the omission of the
1a4e2c4
package is not logged in any way at all, and they have no way to
1a4e2c4
find out about it.
1a4e2c4
1a4e2c4
If `strict` is set to `False`, the current behaviour described
1a4e2c4
above will be used: non-installable packages will be skipped. We
1a4e2c4
implement this by restoring the old separation between `install`
1a4e2c4
and `install_opt` which was removed in 1e19018. When group_install
1a4e2c4
is called with `strict=True` we add the packages to the `install`
1a4e2c4
set; when it's called with `strict=False` we add the packages to
1a4e2c4
the `install_opt` set. Then `_finalize_comps_trans` requests
1a4e2c4
strict behaviour for `install` and non-strict for `install_opt`.
1a4e2c4
1a4e2c4
It continues to be the case that the comps 'type' - mandatory,
1a4e2c4
default, or optional - does not matter; the same behaviour will be
1a4e2c4
used for all three. Again, this is how yum behaved (in the
1a4e2c4
absence of --skip-broken)
1a4e2c4
1a4e2c4
The comps 'types' were *never* intended to dictate yum/dnf
1a4e2c4
behaviour in terms of whether it should fail on error or not, as I
1a4e2c4
understand it; they were in fact related to installer UI behaviour.
1a4e2c4
In the old installer UI, after you selected a group, you would see
1a4e2c4
all the packages in the group listed. 'mandatory' packages would
1a4e2c4
be pre-checked for installation and the check would be greyed out
1a4e2c4
- you couldn't unselect them. 'default' packages would be
1a4e2c4
pre-checked for installation, but you *could* de-select them if you
1a4e2c4
wanted. 'optional' packages would not be pre-checked for
1a4e2c4
installation, but you could *select* them if you wanted. This was
1a4e2c4
the intent of the comps 'type', as I understand it.
1a4e2c4
1a4e2c4
Importantly, a package not existing at all will continue to *not*
1a4e2c4
be a fatal error whatever `strict` is set to. as this is handled
1a4e2c4
*before* we reach the `trans_install` function.
1a4e2c4
1a4e2c4
Signed-off-by: Adam Williamson <awilliam@redhat.com>
1a4e2c4
---
1a4e2c4
 dnf/base.py                   |  21 ++++--
1a4e2c4
 dnf/comps.py                  |  27 +++++++-
1a4e2c4
 tests/repos/broken_group.repo |   4 ++
1a4e2c4
 tests/repos/main_comps.xml    |   4 +-
1a4e2c4
 tests/test_groups.py          | 122 +++++++++++++++++++++++++++-------
1a4e2c4
 5 files changed, 145 insertions(+), 33 deletions(-)
1a4e2c4
 create mode 100644 tests/repos/broken_group.repo
1a4e2c4
1a4e2c4
diff --git a/dnf/base.py b/dnf/base.py
1a4e2c4
index 88e9467a..dacc42d9 100644
1a4e2c4
--- a/dnf/base.py
1a4e2c4
+++ b/dnf/base.py
1a4e2c4
@@ -1450,17 +1450,17 @@ class Base(object):
1a4e2c4
             self._goal.upgrade(select=sltr)
1a4e2c4
             return remove_query
1a4e2c4
 
1a4e2c4
-        def trans_install(query, remove_query, comps_pkg):
1a4e2c4
+        def trans_install(query, remove_query, comps_pkg, strict):
1a4e2c4
             if self.conf.multilib_policy == "all":
1a4e2c4
                 if not comps_pkg.requires:
1a4e2c4
-                    self._install_multiarch(query, strict=False)
1a4e2c4
+                    self._install_multiarch(query, strict=strict)
1a4e2c4
                 else:
1a4e2c4
                     # it installs only one arch for conditional packages
1a4e2c4
                     installed_query = query.installed().apply()
1a4e2c4
                     self._report_already_installed(installed_query)
1a4e2c4
                     sltr = dnf.selector.Selector(self.sack)
1a4e2c4
                     sltr.set(provides="({} if {})".format(comps_pkg.name, comps_pkg.requires))
1a4e2c4
-                    self._goal.install(select=sltr, optional=True)
1a4e2c4
+                    self._goal.install(select=sltr, optional=not strict)
1a4e2c4
 
1a4e2c4
             else:
1a4e2c4
                 sltr = dnf.selector.Selector(self.sack)
1a4e2c4
@@ -1468,7 +1468,7 @@ class Base(object):
1a4e2c4
                     sltr.set(provides="({} if {})".format(comps_pkg.name, comps_pkg.requires))
1a4e2c4
                 else:
1a4e2c4
                     sltr.set(pkg=query)
1a4e2c4
-                self._goal.install(select=sltr, optional=True)
1a4e2c4
+                self._goal.install(select=sltr, optional=not strict)
1a4e2c4
             return remove_query
1a4e2c4
 
1a4e2c4
         def trans_remove(query, remove_query, comps_pkg):
1a4e2c4
@@ -1476,7 +1476,8 @@ class Base(object):
1a4e2c4
             return remove_query
1a4e2c4
 
1a4e2c4
         remove_query = self.sack.query().filterm(empty=True)
1a4e2c4
-        attr_fn = ((trans.install, trans_install),
1a4e2c4
+        attr_fn = ((trans.install, functools.partial(trans_install, strict=True)),
1a4e2c4
+                   (trans.install_opt, functools.partial(trans_install, strict=False)),
1a4e2c4
                    (trans.upgrade, trans_upgrade),
1a4e2c4
                    (trans.remove, trans_remove))
1a4e2c4
 
1a4e2c4
@@ -1547,6 +1548,10 @@ class Base(object):
1a4e2c4
         """Installs packages of selected group
1a4e2c4
         :param exclude: list of package name glob patterns
1a4e2c4
             that will be excluded from install set
1a4e2c4
+        :param strict: boolean indicating whether group packages that
1a4e2c4
+            exist but are non-installable due to e.g. dependency
1a4e2c4
+            issues should be skipped (False) or cause transaction to
1a4e2c4
+            fail to resolve (True)
1a4e2c4
         """
1a4e2c4
         def _pattern_to_pkgname(pattern):
1a4e2c4
             if dnf.util.is_glob_pattern(pattern):
1a4e2c4
@@ -1568,8 +1573,12 @@ class Base(object):
1a4e2c4
                                           strict)
1a4e2c4
         if not trans:
1a4e2c4
             return 0
1a4e2c4
+        if strict:
1a4e2c4
+            instlog = trans.install
1a4e2c4
+        else:
1a4e2c4
+            instlog = trans.install_opt
1a4e2c4
         logger.debug(_("Adding packages from group '%s': %s"),
1a4e2c4
-                     grp_id, trans.install)
1a4e2c4
+                     grp_id, instlog)
1a4e2c4
         return self._add_comps_trans(trans)
1a4e2c4
 
1a4e2c4
     def env_group_install(self, patterns, types, strict=True, exclude=None, exclude_groups=None):
1a4e2c4
diff --git a/dnf/comps.py b/dnf/comps.py
1a4e2c4
index 079d21be..8743812a 100644
1a4e2c4
--- a/dnf/comps.py
1a4e2c4
+++ b/dnf/comps.py
1a4e2c4
@@ -476,18 +476,20 @@ class CompsTransPkg(object):
1a4e2c4
 class TransactionBunch(object):
1a4e2c4
     def __init__(self):
1a4e2c4
         self._install = set()
1a4e2c4
+        self._install_opt = set()
1a4e2c4
         self._remove = set()
1a4e2c4
         self._upgrade = set()
1a4e2c4
 
1a4e2c4
     def __iadd__(self, other):
1a4e2c4
         self._install.update(other._install)
1a4e2c4
+        self._install_opt.update(other._install_opt)
1a4e2c4
         self._upgrade.update(other._upgrade)
1a4e2c4
         self._remove = (self._remove | other._remove) - \
1a4e2c4
-            self._install - self._upgrade
1a4e2c4
+            self._install - self._install_opt - self._upgrade
1a4e2c4
         return self
1a4e2c4
 
1a4e2c4
     def __len__(self):
1a4e2c4
-        return len(self.install) + len(self.upgrade) + len(self.remove)
1a4e2c4
+        return len(self.install) + len(self.install_opt) + len(self.upgrade) + len(self.remove)
1a4e2c4
 
1a4e2c4
     @staticmethod
1a4e2c4
     def _set_value(param, val):
1a4e2c4
@@ -499,12 +501,28 @@ class TransactionBunch(object):
1a4e2c4
 
1a4e2c4
     @property
1a4e2c4
     def install(self):
1a4e2c4
+        """
1a4e2c4
+        Packages to be installed with strict=True - transaction will
1a4e2c4
+        fail if they cannot be installed due to dependency errors etc.
1a4e2c4
+        """
1a4e2c4
         return self._install
1a4e2c4
 
1a4e2c4
     @install.setter
1a4e2c4
     def install(self, value):
1a4e2c4
         self._set_value(self._install, value)
1a4e2c4
 
1a4e2c4
+    @property
1a4e2c4
+    def install_opt(self):
1a4e2c4
+        """
1a4e2c4
+        Packages to be installed with strict=False - they will be
1a4e2c4
+        skipped if they cannot be installed
1a4e2c4
+        """
1a4e2c4
+        return self._install_opt
1a4e2c4
+
1a4e2c4
+    @install_opt.setter
1a4e2c4
+    def install_opt(self, value):
1a4e2c4
+        self._set_value(self._install_opt, value)
1a4e2c4
+
1a4e2c4
     @property
1a4e2c4
     def remove(self):
1a4e2c4
         return self._remove
1a4e2c4
@@ -641,7 +659,10 @@ class Solver(object):
1a4e2c4
 
1a4e2c4
         trans = TransactionBunch()
1a4e2c4
         # TODO: remove exclude
1a4e2c4
-        trans.install.update(self._pkgs_of_type(comps_group, pkg_types, exclude=[]))
1a4e2c4
+        if strict:
1a4e2c4
+            trans.install.update(self._pkgs_of_type(comps_group, pkg_types, exclude=[]))
1a4e2c4
+        else:
1a4e2c4
+            trans.install_opt.update(self._pkgs_of_type(comps_group, pkg_types, exclude=[]))
1a4e2c4
         return trans
1a4e2c4
 
1a4e2c4
     def _group_remove(self, group_id):
1a4e2c4
diff --git a/tests/repos/broken_group.repo b/tests/repos/broken_group.repo
1a4e2c4
new file mode 100644
1a4e2c4
index 00000000..7f151971
1a4e2c4
--- /dev/null
1a4e2c4
+++ b/tests/repos/broken_group.repo
1a4e2c4
@@ -0,0 +1,4 @@
1a4e2c4
+=Ver: 2.0
1a4e2c4
+#
1a4e2c4
+=Pkg: brokendeps 20 2 x86_64
1a4e2c4
+=Req: nosuchpackage >= 1.2-0
1a4e2c4
diff --git a/tests/repos/main_comps.xml b/tests/repos/main_comps.xml
1a4e2c4
index 3cf8faa5..9e694d13 100644
1a4e2c4
--- a/tests/repos/main_comps.xml
1a4e2c4
+++ b/tests/repos/main_comps.xml
1a4e2c4
@@ -44,7 +44,9 @@
1a4e2c4
     <name>Broken Group</name>
1a4e2c4
     <packagelist>
1a4e2c4
       <packagereq type="mandatory">meaning-of-life</packagereq>
1a4e2c4
-      <packagereq>lotus</packagereq>
1a4e2c4
+      <packagereq type="mandatory">lotus</packagereq>
1a4e2c4
+      <packagereq type="default" requires="no-such-package">librita</packagereq>
1a4e2c4
+      <packagereq type="optional">brokendeps</packagereq>
1a4e2c4
     </packagelist>
1a4e2c4
   </group>
1a4e2c4
   <category>
1a4e2c4
diff --git a/tests/test_groups.py b/tests/test_groups.py
1a4e2c4
index ec10a619..fe388f96 100644
1a4e2c4
--- a/tests/test_groups.py
1a4e2c4
+++ b/tests/test_groups.py
1a4e2c4
@@ -183,29 +183,6 @@ class PresetPersistorTest(tests.support.ResultTestCase):
1a4e2c4
         swdb_group = self.history.group.get(comps_group.id)
1a4e2c4
         self.assertIsNotNone(swdb_group)
1a4e2c4
 
1a4e2c4
-    """
1a4e2c4
-    this should be reconsidered once relengs document comps
1a4e2c4
-    def test_group_install_broken(self):
1a4e2c4
-        grp = self.base.comps.group_by_pattern('Broken Group')
1a4e2c4
-        p_grp = self.history.group.get('broken-group')
1a4e2c4
-        self.assertFalse(p_grp.installed)
1a4e2c4
-
1a4e2c4
-        self.assertRaises(dnf.exceptions.MarkingError,
1a4e2c4
-                          self.base.group_install, grp.id,
1a4e2c4
-                          ('mandatory', 'default'))
1a4e2c4
-        p_grp = self.history.group.get('broken-group')
1a4e2c4
-        self.assertFalse(p_grp.installed)
1a4e2c4
-
1a4e2c4
-        self.assertEqual(self.base.group_install(grp.id,
1a4e2c4
-                                                 ('mandatory', 'default'),
1a4e2c4
-                                                 strict=False), 1)
1a4e2c4
-        inst, removed = self.installed_removed(self.base)
1a4e2c4
-        self.assertLength(inst, 1)
1a4e2c4
-        self.assertEmpty(removed)
1a4e2c4
-        p_grp = self.history.group.get('broken-group')
1a4e2c4
-        self.assertTrue(p_grp.installed)
1a4e2c4
-    """
1a4e2c4
-
1a4e2c4
     def test_group_remove(self):
1a4e2c4
         self._install_test_group()
1a4e2c4
         group_id = 'somerset'
1a4e2c4
@@ -220,6 +197,105 @@ class PresetPersistorTest(tests.support.ResultTestCase):
1a4e2c4
         self._swdb_end()
1a4e2c4
 
1a4e2c4
 
1a4e2c4
+class ProblemGroupTest(tests.support.ResultTestCase):
1a4e2c4
+    """Test some cases involving problems in groups: packages that
1a4e2c4
+    don't exist, and packages that exist but cannot be installed. The
1a4e2c4
+    "broken" group lists three packages. "meaning-of-life", explicitly
1a4e2c4
+    'default', does not exist. "lotus", implicitly 'mandatory' (no
1a4e2c4
+    explicit type), exists and is installable. "brokendeps",
1a4e2c4
+    explicitly 'optional', exists but has broken dependencies. See
1a4e2c4
+    https://bugzilla.redhat.com/show_bug.cgi?id=1292892,
1a4e2c4
+    https://bugzilla.redhat.com/show_bug.cgi?id=1337731,
1a4e2c4
+    https://bugzilla.redhat.com/show_bug.cgi?id=1427365, and
1a4e2c4
+    https://bugzilla.redhat.com/show_bug.cgi?id=1461539 for some of
1a4e2c4
+    the background on this.
1a4e2c4
+    """
1a4e2c4
+
1a4e2c4
+    REPOS = ['main', 'broken_group']
1a4e2c4
+    COMPS = True
1a4e2c4
+    COMPS_SEED_PERSISTOR = True
1a4e2c4
+
1a4e2c4
+    def test_group_install_broken_mandatory(self):
1a4e2c4
+        """Here we will test installing the group with only mandatory
1a4e2c4
+        packages. We expect this to succeed, leaving out the
1a4e2c4
+        non-existent 'meaning-of-life': it should also log a warning,
1a4e2c4
+        but we don't test that.
1a4e2c4
+        """
1a4e2c4
+        comps_group = self.base.comps.group_by_pattern('Broken Group')
1a4e2c4
+        swdb_group = self.history.group.get(comps_group.id)
1a4e2c4
+        self.assertIsNone(swdb_group)
1a4e2c4
+
1a4e2c4
+        cnt = self.base.group_install(comps_group.id, ('mandatory'))
1a4e2c4
+        self._swdb_commit()
1a4e2c4
+        self.base.resolve()
1a4e2c4
+        # this counts packages *listed* in the group, so 2
1a4e2c4
+        self.assertEqual(cnt, 2)
1a4e2c4
+
1a4e2c4
+        inst, removed = self.installed_removed(self.base)
1a4e2c4
+        # the above should work, but only 'lotus' actually installed
1a4e2c4
+        self.assertLength(inst, 1)
1a4e2c4
+        self.assertEmpty(removed)
1a4e2c4
+
1a4e2c4
+    def test_group_install_broken_default(self):
1a4e2c4
+        """Here we will test installing the group with only mandatory
1a4e2c4
+        and default packages. Again we expect this to succeed: the new
1a4e2c4
+        factor is an entry pulling in librita if no-such-package is
1a4e2c4
+        also included or installed. We expect this not to actually
1a4e2c4
+        pull in librita (as no-such-package obviously *isn't* there),
1a4e2c4
+        but also not to cause a fatal error.
1a4e2c4
+        """
1a4e2c4
+        comps_group = self.base.comps.group_by_pattern('Broken Group')
1a4e2c4
+        swdb_group = self.history.group.get(comps_group.id)
1a4e2c4
+        self.assertIsNone(swdb_group)
1a4e2c4
+
1a4e2c4
+        cnt = self.base.group_install(comps_group.id, ('mandatory', 'default'))
1a4e2c4
+        self._swdb_commit()
1a4e2c4
+        self.base.resolve()
1a4e2c4
+        # this counts packages *listed* in the group, so 3
1a4e2c4
+        self.assertEqual(cnt, 3)
1a4e2c4
+
1a4e2c4
+        inst, removed = self.installed_removed(self.base)
1a4e2c4
+        # the above should work, but only 'lotus' actually installed
1a4e2c4
+        self.assertLength(inst, 1)
1a4e2c4
+        self.assertEmpty(removed)
1a4e2c4
+
1a4e2c4
+    def test_group_install_broken_optional(self):
1a4e2c4
+        """Here we test installing the group with optional packages
1a4e2c4
+        included. We expect this to fail, as a package that exists but
1a4e2c4
+        has broken dependencies is now included.
1a4e2c4
+        """
1a4e2c4
+        comps_group = self.base.comps.group_by_pattern('Broken Group')
1a4e2c4
+        swdb_group = self.history.group.get(comps_group.id)
1a4e2c4
+        self.assertIsNone(swdb_group)
1a4e2c4
+
1a4e2c4
+        cnt = self.base.group_install(comps_group.id, ('mandatory', 'default', 'optional'))
1a4e2c4
+        self.assertEqual(cnt, 4)
1a4e2c4
+
1a4e2c4
+        self._swdb_commit()
1a4e2c4
+        # this should fail, as optional 'brokendeps' is now pulled in
1a4e2c4
+        self.assertRaises(dnf.exceptions.DepsolveError, self.base.resolve)
1a4e2c4
+
1a4e2c4
+    def test_group_install_broken_optional_nonstrict(self):
1a4e2c4
+        """Here we test installing the group with optional packages
1a4e2c4
+        included, but with strict=False. We expect this to succeed,
1a4e2c4
+        skipping the package with broken dependencies.
1a4e2c4
+        """
1a4e2c4
+        comps_group = self.base.comps.group_by_pattern('Broken Group')
1a4e2c4
+        swdb_group = self.history.group.get(comps_group.id)
1a4e2c4
+        self.assertIsNone(swdb_group)
1a4e2c4
+
1a4e2c4
+        cnt = self.base.group_install(comps_group.id, ('mandatory', 'default', 'optional'),
1a4e2c4
+                                      strict=False)
1a4e2c4
+        self._swdb_commit()
1a4e2c4
+        self.base.resolve()
1a4e2c4
+        self.assertEqual(cnt, 4)
1a4e2c4
+
1a4e2c4
+        inst, removed = self.installed_removed(self.base)
1a4e2c4
+        # the above should work, but only 'lotus' actually installed
1a4e2c4
+        self.assertLength(inst, 1)
1a4e2c4
+        self.assertEmpty(removed)
1a4e2c4
+
1a4e2c4
+
1a4e2c4
 class EnvironmentInstallTest(tests.support.ResultTestCase):
1a4e2c4
     """Set up a test where sugar is considered not installed."""
1a4e2c4
 
1a4e2c4
-- 
1a4e2c4
2.19.0
1a4e2c4