Skip to content

Commit

Permalink
Group transactions: optionally report missing group packages
Browse files Browse the repository at this point in the history
This modifies `base.resolve()` so a caller may pass in a dict.
If so, the dict will be populated with information on any
'missing' group packages: that is, cases where a group that
was included in the transaction specifies a package name, but
no package by that name could be found in the repositories.

The dict has four keys matching the four libcomps package
'types', and the value for each key is a list of the missing
package names of that type (if any).

The intent of this change is to enable callers to treat
missing group packages as a fatal error (or do anything else
they like with the information), without changing default
behaviour, API, or the behaviour of the dnf CLI.

Links to some of the the history behind this change:
https://bugzilla.redhat.com/show_bug.cgi?id=1292892
https://bugzilla.redhat.com/show_bug.cgi?id=1427365
https://bugzilla.redhat.com/show_bug.cgi?id=1461539
#1038

Signed-off-by: Adam Williamson <[email protected]>
  • Loading branch information
AdamWill committed Sep 7, 2022
1 parent b623eed commit d20f811
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 11 deletions.
16 changes: 14 additions & 2 deletions dnf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
import gc
import hawkey
import itertools
import libcomps
import logging
import math
import os
Expand Down Expand Up @@ -880,11 +881,14 @@ def _set_excludes_from_weak_to_goal(self):
query = query.available()
self._goal.add_exclude_from_weak(query)

def resolve(self, allow_erasing=False):
def resolve(self, allow_erasing=False, missing_dict=None):
# :api
"""Build the transaction set."""
exc = None
self._finalize_comps_trans()
missing_group_pkgs = self._finalize_comps_trans()
if isinstance(missing_dict, dict):
for (key, value) in missing_group_pkgs.items():
missing_dict[key] = value

timer = dnf.logging.Timer('depsolve')
self._ds_callback.start()
Expand Down Expand Up @@ -1695,6 +1699,12 @@ def trans_remove(query, remove_query, comps_pkg):
(trans.upgrade, trans_upgrade),
(trans.remove, trans_remove))

missing_group_pkgs = {
libcomps.PACKAGE_TYPE_OPTIONAL: [],
libcomps.PACKAGE_TYPE_DEFAULT: [],
libcomps.PACKAGE_TYPE_MANDATORY: [],
libcomps.PACKAGE_TYPE_CONDITIONAL: []
}
for (attr, fn) in attr_fn:
for comps_pkg in attr:
query_args = {'name': comps_pkg.name}
Expand All @@ -1707,11 +1717,13 @@ def trans_remove(query, remove_query, comps_pkg):
if comps_pkg.basearchonly:
package_string += '.' + basearch
logger.warning(_('No match for group package "{}"').format(package_string))
missing_group_pkgs[comps_pkg.type].append(package_string)
continue
remove_query = fn(q, remove_query, comps_pkg)
self._goal.group_members.add(comps_pkg.name)

self._remove_if_unneeded(remove_query)
return missing_group_pkgs

def _build_comps_solver(self):
def reason_fn(pkgname):
Expand Down
43 changes: 34 additions & 9 deletions tests/test_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import operator

import libcomps
import libdnf.transaction

import dnf.comps
Expand Down Expand Up @@ -200,10 +201,12 @@ def test_group_remove(self):
class ProblemGroupTest(tests.support.ResultTestCase):
"""Test some cases involving problems in groups: packages that
don't exist, and packages that exist but cannot be installed. The
"broken" group lists three packages. "meaning-of-life", explicitly
'default', does not exist. "lotus", implicitly 'mandatory' (no
explicit type), exists and is installable. "brokendeps",
explicitly 'optional', exists but has broken dependencies. See
"broken" group lists four packages. "meaning-of-life", explicitly
'mandatory', does not exist. "lotus", explicitly 'mandatory' (no
explicit type), exists and is installable. "librita", explicitly
'default', exists, but requires non-existent "no-such-package".
"brokendeps", explicitly 'optional', exists but has broken
dependencies. See
https://bugzilla.redhat.com/show_bug.cgi?id=1292892,
https://bugzilla.redhat.com/show_bug.cgi?id=1337731,
https://bugzilla.redhat.com/show_bug.cgi?id=1427365, and
Expand All @@ -218,46 +221,68 @@ class ProblemGroupTest(tests.support.ResultTestCase):
def test_group_install_broken_mandatory(self):
"""Here we will test installing the group with only mandatory
packages. We expect this to succeed, leaving out the
non-existent 'meaning-of-life': it should also log a warning,
but we don't test that.
non-existent 'meaning-of-life', and populating the 'missing
dict' with information on the missing package. It should also
log a warning, but we don't test that.
"""
comps_group = self.base.comps.group_by_pattern('Broken Group')
swdb_group = self.history.group.get(comps_group.id)
self.assertIsNone(swdb_group)

cnt = self.base.group_install(comps_group.id, ('mandatory',))
self._swdb_commit()
self.base.resolve()
missing_dict = {}
self.base.resolve(missing_dict=missing_dict)
# this counts packages *listed* in the group, so 2
self.assertEqual(cnt, 2)

inst, removed = self.installed_removed(self.base)
# the above should work, but only 'lotus' actually installed
self.assertLength(inst, 1)
self.assertEmpty(removed)
# the dict should record that 'meaning-of-life' was missing
expected_dict = {
libcomps.PACKAGE_TYPE_OPTIONAL: [],
libcomps.PACKAGE_TYPE_DEFAULT: [],
libcomps.PACKAGE_TYPE_MANDATORY: ['meaning-of-life'],
libcomps.PACKAGE_TYPE_CONDITIONAL: []
}
self.assertEqual(missing_dict, expected_dict)

def test_group_install_broken_default(self):
"""Here we will test installing the group with only mandatory
and default packages. Again we expect this to succeed: the new
factor is an entry pulling in librita if no-such-package is
also included or installed. We expect this not to actually
pull in librita (as no-such-package obviously *isn't* there),
but also not to cause a fatal error.
but also not to cause a fatal error. We also expect the
missing dict to record 'meaning-of-life', but not 'librita',
as missing.
"""
comps_group = self.base.comps.group_by_pattern('Broken Group')
swdb_group = self.history.group.get(comps_group.id)
self.assertIsNone(swdb_group)

cnt = self.base.group_install(comps_group.id, ('mandatory', 'default'))
self._swdb_commit()
self.base.resolve()
missing_dict = {}
self.base.resolve(missing_dict=missing_dict)
# this counts packages *listed* in the group, so 3
self.assertEqual(cnt, 3)

inst, removed = self.installed_removed(self.base)
# the above should work, but only 'lotus' actually installed
self.assertLength(inst, 1)
self.assertEmpty(removed)
# the dict should record that 'meaning-of-life' was missing,
# but *not* librita
expected_dict = {
libcomps.PACKAGE_TYPE_OPTIONAL: [],
libcomps.PACKAGE_TYPE_DEFAULT: [],
libcomps.PACKAGE_TYPE_MANDATORY: ['meaning-of-life'],
libcomps.PACKAGE_TYPE_CONDITIONAL: []
}
self.assertEqual(missing_dict, expected_dict)

def test_group_install_broken_optional(self):
"""Here we test installing the group with optional packages
Expand Down

0 comments on commit d20f811

Please sign in to comment.