PR#4397: Clean up cli for comps management

Merges #4397
https://pagure.io/koji/pull-request/4397

Fixes: #3199
https://pagure.io/koji/issue/3199
RFE: remove-group-pkg

Fixes: #4393
https://pagure.io/koji/issue/4393
Removing a package from a group doesn't work
This commit is contained in:
Mike McLean 2025-07-22 10:21:24 -04:00
commit 311dfde77b
10 changed files with 864 additions and 20 deletions

View file

@ -111,18 +111,38 @@ def handle_block_group(goptions, session, args):
if not (session.hasPerm('admin') or session.hasPerm('tag')):
parser.error("This action requires tag or admin privileges")
dsttag = session.getTag(tag)
if not dsttag:
taginfo = session.getTag(tag)
if not taginfo:
error("No such tag: %s" % tag)
groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)])
group_id = groups.get(group, None)
if group_id is None:
# sanity check
groups = session.getTagGroups(taginfo['id'], incl_pkgs=False, incl_reqs=False,
incl_blocked=True)
for ginfo in groups:
if ginfo['name'] == group:
break
else:
error("Group %s doesn't exist within tag %s" % (group, tag))
if ginfo['blocked']:
error("Group %s is already blocked in this tag" % group)
# (we don't care if the entry is inherited for this)
session.groupListBlock(tag, group)
def handle_unblock_group(goptions, session, args):
"[admin] Unblock a group from tag"
usage = "usage: %prog unblock-group [options] <tag> <group>"
parser = OptionParser(usage=get_usage_str(usage))
(options, args) = parser.parse_args(args)
if len(args) != 2:
parser.error("You must specify a tag name and group name")
tag = args[0]
group = args[1]
activate_session(session, goptions)
session.groupListUnblock(tag, group)
def handle_remove_group(goptions, session, args):
"[admin] Remove group from tag"
usage = "usage: %prog remove-group <tag> <group>"
@ -137,14 +157,25 @@ def handle_remove_group(goptions, session, args):
if not (session.hasPerm('admin') or session.hasPerm('tag')):
parser.error("This action requires tag or admin privileges")
dsttag = session.getTag(tag)
if not dsttag:
taginfo = session.getTag(tag)
if not taginfo:
error("No such tag: %s" % tag)
groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)])
group_id = groups.get(group, None)
if group_id is None:
# sanity checks
groups = session.getTagGroups(taginfo['id'], incl_pkgs=False, incl_reqs=False,
incl_blocked=True)
for ginfo in groups:
if ginfo['name'] == group:
break
else:
error("Group %s doesn't exist within tag %s" % (group, tag))
if ginfo['blocked']:
error("Group %s is blocked in this tag. You could use unblock-group to unblock it" % group)
if ginfo['tag_id'] != taginfo['id']:
# listing is inherited
srctag = session.getTag(ginfo['tag_id'])
error("The entry for group %s is inherited from %s. "
"You could use block-group to prevent this" % (group, srctag['name']))
session.groupListRemove(tag, group)
@ -3100,6 +3131,55 @@ def handle_add_group_pkg(goptions, session, args):
session.groupPackageListAdd(tag, group, pkg)
def handle_remove_group_pkg(goptions, session, args):
"[admin] Remove packages from a group's package listing"
usage = "usage: %prog remove-group-pkg [options] <tag> <group> <pkg> [<pkg> ...]"
parser = OptionParser(usage=get_usage_str(usage))
(options, args) = parser.parse_args(args)
if len(args) < 3:
parser.error("You must specify a tag name, group name, and one or more package names")
tag = args[0]
group = args[1]
packages = args[2:]
activate_session(session, goptions)
taginfo = session.getTag(tag)
if taginfo is None:
error("No such tag: %s" % tag)
# sanity checks
groups = session.getTagGroups(taginfo['id'], incl_reqs=False, incl_blocked=True)
for ginfo in groups:
if ginfo['name'] == group:
break
else:
error("Group %s is not present in tag %s" % (group, tag))
pkg_idx = {p['package']: p for p in ginfo['packagelist']}
sane = True
for pkg in packages:
if pkg not in pkg_idx:
print("Package %s is not included in this group" % pkg)
sane = False
continue
pinfo = pkg_idx[pkg]
if pinfo['blocked']:
print("Package %s is blocked in this group. "
"You could use unblock-group-pkg to unblock it" % pkg)
sane = False
continue
if pinfo['tag_id'] != taginfo['id']:
# listing is inherited
srctag = session.getTag(pinfo['tag_id'])
print("The entry for package %s is inherited from %s. "
"You could use block-group-pkg to prevent this" % (pkg, srctag['name']))
sane = False
continue
if not sane:
error('Invalid parameters')
with session.multicall() as m:
[m.groupPackageListRemove(taginfo['id'], group, pkg) for pkg in packages]
def handle_block_group_pkg(goptions, session, args):
"[admin] Block a package from a group's package listing"
usage = "usage: %prog block-group-pkg [options] <tag> <group> <pkg> [<pkg> ...]"
@ -3172,6 +3252,55 @@ def handle_unblock_group_req(goptions, session, args):
session.groupReqListUnblock(tag, group, req)
def handle_remove_group_req(goptions, session, args):
"[admin] Remove entries from a group's requirement listing"
usage = "usage: %prog remove-group-req [options] <tag> <group> <req> [<req> ...]"
parser = OptionParser(usage=get_usage_str(usage))
(options, args) = parser.parse_args(args)
if len(args) < 3:
parser.error("You must specify a tag name, group name, and one or more requirement names")
tag = args[0]
group = args[1]
requires = args[2:]
activate_session(session, goptions)
taginfo = session.getTag(tag)
if taginfo is None:
error("No such tag: %s" % tag)
# sanity checks
groups = session.getTagGroups(taginfo['id'], incl_pkgs=False, incl_blocked=True)
for ginfo in groups:
if ginfo['name'] == group:
break
else:
error("Group %s is not present in tag %s" % (group, tag))
req_idx = {r['name']: r for r in ginfo['grouplist']}
sane = True
for req in requires:
if req not in req_idx:
print("Req %s is not included in this group" % req)
sane = False
continue
rinfo = req_idx[req]
if rinfo['blocked']:
print("Req %s is blocked in this group. "
"You could use unblock-group-req to unblock it" % req)
sane = False
continue
if rinfo['tag_id'] != taginfo['id']:
# listing is inherited
srctag = session.getTag(rinfo['tag_id'])
print("The entry for req %s is inherited from %s. "
"You could use block-group-req to prevent this" % (req, srctag['name']))
sane = False
continue
if not sane:
error('Invalid parameters')
with session.multicall() as m:
[m.groupReqListRemove(taginfo['id'], group, req) for req in requires]
def anon_handle_list_channels(goptions, session, args):
"[info] Print channels listing"
usage = "usage: %prog list-channels [options]"

View file

@ -33,6 +33,7 @@ Contents
repo_generation
exporting_repositories
tag_inheritance
managing_comps_data
misc
release_notes/release_notes
migrations/migrations

View file

@ -0,0 +1,157 @@
Managing comps data for tags
============================
Koji can record comps data for each tag and uses that data to write a comps
file whenever it generates a yum repo for that tag. This document describes
how this data is stored, how to read it, and how to change it.
For background on comps files, see
* https://dnf5.readthedocs.io/en/latest/misc/comps.7.html
* https://fedoraproject.org/wiki/How_to_use_and_edit_comps.xml_for_package_groups
The comps data *affects how rpm builds are performed*.
Various buildroots generated by Koji use these groups for their base install.
E.g.
* rpm builds use the ``build`` group
* source rpm builds use the ``srpm-build`` group
* maven builds use the ``maven-build`` group
* wrapper rpm builds use the ``wrapper-rpm-build`` group
* appliance builds use the ``appliance-build`` group
* livecd builds use the ``livecd-build`` group
* livemedia builds use the ``livemedia-build`` group
Reading the data
----------------
The ``koji list-groups`` cli command will output the comps data for a given tag
.. code-block:: text
Usage: koji list-groups [options] <tag> [<group>]
(Specify the --help global option for a list of other help options)
Options:
-h, --help show this help message and exit
--event=EVENT# query at event
--ts=TIMESTAMP query at last event before timestamp
--repo=REPO# query at event for a repo
--show-blocked Show blocked packages and groups
The command will optionally accept a group name to limit the output to a single group.
Like many other types of data in Koji, the comps data is versioned and Koji remembers its history.
The ``--event/--ts/--repo`` options can be used to query this data from an earlier point in time.
The ``show-blocked`` option will include blocked entries in the output.
Alternately ``show-groups`` command can be used to present the groups data in different formats.
This command is a historical oddity, but still useful to export the data in comps format.
Example
-------
The following command prints the srpm-build group data for the f41-build tag.
.. code-block:: text
$ koji list-groups f41-build srpm-build
srpm-build [f41]
bash: None, mandatory [f41]
fedora-release: None, mandatory [f41]
fedpkg-minimal: None, mandatory [f41]
glibc-minimal-langpack: None, mandatory [f41]
gnupg2: None, mandatory [f41]
redhat-rpm-config: None, mandatory [f41]
rpm-build: None, mandatory [f41]
shadow-utils: None, mandatory [f41]
Because our command included a specific group name, the command only outputs that one group.
Under this group, we see the list of packages in the group and their parameters.
The bracketed value at the end shows the tag that the entry is inherited from.
Groups data and inheritance
---------------------------
Like most other data associated with tags, groups data is inheritable.
Because the groups data is complex, so is the inheritance.
The groups data is inherited piecemeal, with individual groups, group package
entries, and group dependencies separately inheritable.
You can prevent an entry from being inherited by blocking it.
This can be done via the ``block-group``, ``block-group-pkg``, and ``block-group-req``
commands respectively (and undone with the corresponding unblock command).
As noted above, you can see which tags an entry comes from via the ``list-groups`` command.
So, a given tag will have a list of groups that includes inherited entries.
Each of those groups will have a list of packages and a list of group dependencies
which include inherited entries.
For admin sanity, we recommend defining a comps in a base tag in your inheritance
and keeping adjustments in child tags to a minimum.
A common case is to add additional packages to the build group for a specialized
build tag.
Changing comps data
-------------------
You can add, remove, block, and unblock individual entries with the relevant command
.. code-block:: text
add-group Add a group to a tag
remove-group Remove group from tag
block-group Block group in tag
unblock-group Unblock a group from tag
add-group-pkg Add a package to a group's package listing
remove-group-pkg Remove packages from a group's package listing
block-group-pkg Block a package from a group's package listing
unblock-group-pkg Unblock a package from a group's package listing
add-group-req Add a group to a group's required list
block-group-req Block a group's requirement listing
remove-group-req Remove entries from a group's requirement listing
unblock-group-req Unblock a group's requirement listing
Additionally, you can also import groups data directly from a comps file with the ``import-comps`` command.
This is useful when setting up a new tag from scratch.
.. code-block:: text
Usage: koji import-comps [options] <file> <tag>
(Specify the --help global option for a list of other help options)
Options:
-h, --help show this help message and exit
--force force import
Blocking vs removing
--------------------
The various remove commands remove a specific entry from a specific tag.
They will fail if the entry is not actually in the specified tag.
This commonly happens when an entry is inherited.
When your tag has an inherited comps entry that you want to remove, you have two
options:
* block the entry in the tag
* remove the entry from the ancestor tag it is inherited from
It is generally safest to block, as this only affects the tag of concern.
Removing the entry in an ancestor could affect other tags negatively.
However, there are certainly cases where it makes sense to remove the entry
from the ancestor.
Group dependencies
------------------
For historical reasons, Koji's comps data allows specifying group dependecies/requirements.
This is antiquated feature of comps that is longer used by dnf,
so in practice there is no need to specify such entries (i.e. the various ``*-req`` commands).

View file

@ -49,6 +49,8 @@ admin commands:
regen-repo Generate a current repo if there is not one
remove-external-repo Remove an external repo from a tag or tags, or remove entirely
remove-group Remove group from tag
remove-group-pkg Remove packages from a group's package listing
remove-group-req Remove entries from a group's requirement listing
remove-host-from-channel Remove a host from a channel
remove-pkg Remove a package from the listing for tag
remove-sig Remove signed RPMs from db and disk
@ -64,6 +66,7 @@ admin commands:
set-pkg-owner Set the owner for a package
set-pkg-owner-global Set the owner for a package globally
set-task-priority Set task priority
unblock-group Unblock a group from tag
unblock-group-pkg Unblock a package from a group's package listing
unblock-group-req Unblock a group's requirement listing
unblock-pkg Unblock a package in the listing for tag

View file

@ -49,6 +49,8 @@ admin commands:
regen-repo Generate a current repo if there is not one
remove-external-repo Remove an external repo from a tag or tags, or remove entirely
remove-group Remove group from tag
remove-group-pkg Remove packages from a group's package listing
remove-group-req Remove entries from a group's requirement listing
remove-host-from-channel Remove a host from a channel
remove-pkg Remove a package from the listing for tag
remove-sig Remove signed RPMs from db and disk
@ -64,6 +66,7 @@ admin commands:
set-pkg-owner Set the owner for a package
set-pkg-owner-global Set the owner for a package globally
set-task-priority Set task priority
unblock-group Unblock a group from tag
unblock-group-pkg Unblock a package from a group's package listing
unblock-group-req Unblock a group's requirement listing
unblock-pkg Unblock a package in the listing for tag

View file

@ -56,10 +56,11 @@ class TestBlockGroup(utils.CliTestCase):
def test_handle_block_group_nonexistent_group(self):
tag = 'tag'
tagID = 100
group = 'group'
arguments = [tag, group]
self.session.hasPerm.return_value = True
self.session.getTag.return_value = tag
self.session.getTag.return_value = {'name': tag, 'id': tagID}
self.session.getTagGroups.return_value = []
# Run it and check immediate output
@ -75,18 +76,19 @@ class TestBlockGroup(utils.CliTestCase):
self.activate_session_mock.assert_called_once_with(self.session, self.options)
self.session.hasPerm.assert_called_once_with('admin')
self.session.getTag.assert_called_once_with(tag)
self.session.getTagGroups.assert_called_once_with(tag, inherit=False)
self.session.getTagGroups.assert_called_once_with(tagID, incl_pkgs=False, incl_reqs=False, incl_blocked=True)
self.session.groupListBlock.assert_not_called()
@mock.patch('sys.stdout', new_callable=six.StringIO)
def test_handle_block_group(self, stdout):
tag = 'tag'
tagID = 100
group = 'group'
arguments = [tag, group]
self.session.hasPerm.return_value = True
self.session.getTag.return_value = tag
self.session.getTag.return_value = {'name': tag, 'id': tagID}
self.session.getTagGroups.return_value = [
{'name': 'group', 'group_id': 'groupId'}]
{'name': 'group', 'group_id': 'groupId', 'blocked': False}]
# Run it and check immediate output
rv = handle_block_group(self.options, self.session, arguments)
@ -98,10 +100,34 @@ class TestBlockGroup(utils.CliTestCase):
self.activate_session_mock.assert_called_once_with(self.session, self.options)
self.session.hasPerm.assert_called_once_with('admin')
self.session.getTag.assert_called_once_with(tag)
self.session.getTagGroups.assert_called_once_with(tag, inherit=False)
self.session.getTagGroups.assert_called_once_with(tagID, incl_pkgs=False, incl_reqs=False, incl_blocked=True)
self.session.groupListBlock.assert_called_once_with(tag, group)
self.assertEqual(rv, None)
def test_handle_group_already_blocked(self):
tag = 'tag'
tagID = 100
group = 'group'
arguments = [tag, group]
self.session.hasPerm.return_value = True
self.session.getTag.return_value = {'name': tag, 'id': tagID}
self.session.getTagGroups.return_value = [
{'name': 'group', 'group_id': 'groupId', 'blocked': True}]
# Run it and check immediate output
self.assert_system_exit(
handle_block_group,
self.options, self.session, arguments,
stderr='Group group is already blocked in this tag\n',
stdout='',
exit_code=1)
# Finally, assert that things were called as we expected.
self.session.hasPerm.assert_called_once_with('admin')
self.session.getTag.assert_called_once_with(tag)
self.session.getTagGroups.assert_called_once_with(tagID, incl_pkgs=False, incl_reqs=False, incl_blocked=True)
self.session.groupListBlock.assert_not_called()
def test_handle_block_group_error_handling(self):
expected = self.format_error_message(
"Please specify a tag name and a group name")

View file

@ -54,12 +54,13 @@ class TestRemoveGroup(utils.CliTestCase):
@mock.patch('koji_cli.commands.activate_session')
def test_handle_remove_group_nonexistent_group(self, activate_session_mock, stdout, stderr):
tag = 'tag'
tagID = 100
group = 'group'
arguments = [tag, group]
# Mock out the xmlrpc server
self.session.hasPerm.return_value = True
self.session.getTag.return_value = tag
self.session.getTag.return_value = {'name': tag, 'id': tagID}
self.session.getTagGroups.return_value = []
with self.assertRaises(SystemExit):
@ -69,7 +70,59 @@ class TestRemoveGroup(utils.CliTestCase):
activate_session_mock.assert_called_once_with(self.session, self.options)
self.session.hasPerm.assert_called_once_with('admin')
self.session.getTag.assert_called_once_with(tag)
self.session.getTagGroups.assert_called_once_with(tag, inherit=False)
self.session.getTagGroups.assert_called_once_with(tagID, incl_pkgs=False, incl_reqs=False, incl_blocked=True)
self.session.groupListRemove.assert_not_called()
@mock.patch('sys.stdout', new_callable=six.StringIO)
@mock.patch('sys.stderr', new_callable=six.StringIO)
@mock.patch('koji_cli.commands.activate_session')
def test_handle_remove_group_blocked_group(self, activate_session_mock, stdout, stderr):
tag = 'tag'
tagID = 100
group = 'group'
arguments = [tag, group]
# Mock out the xmlrpc server
self.session.hasPerm.return_value = True
self.session.getTag.return_value = {'name': tag, 'id': tagID}
self.session.getTagGroups.return_value = [{'name': 'group', 'tag_id': tagID,
'blocked': True}]
with self.assertRaises(SystemExit):
handle_remove_group(self.options, self.session, arguments)
# assert that things were called as we expected.
activate_session_mock.assert_called_once_with(self.session, self.options)
self.session.hasPerm.assert_called_once_with('admin')
self.session.getTag.assert_called_once_with(tag)
self.session.getTagGroups.assert_called_once_with(tagID, incl_pkgs=False, incl_reqs=False, incl_blocked=True)
self.session.groupListRemove.assert_not_called()
@mock.patch('sys.stdout', new_callable=six.StringIO)
@mock.patch('sys.stderr', new_callable=six.StringIO)
@mock.patch('koji_cli.commands.activate_session')
def test_handle_remove_group_inherited_group(self, activate_session_mock, stdout, stderr):
tag = 'tag'
tagID = 100
parentID = 99
group = 'group'
arguments = [tag, group]
# Mock out the xmlrpc server
self.session.hasPerm.return_value = True
self.session.getTag.return_value = {'name': tag, 'id': tagID}
self.session.getTagGroups.return_value = [{'name': 'group', 'tag_id': parentID,
'blocked': False}]
with self.assertRaises(SystemExit):
handle_remove_group(self.options, self.session, arguments)
# assert that things were called as we expected.
activate_session_mock.assert_called_once_with(self.session, self.options)
self.session.hasPerm.assert_called_once_with('admin')
self.session.getTagGroups.assert_called_once_with(tagID, incl_pkgs=False, incl_reqs=False, incl_blocked=True)
expected_calls = [mock.call(tag), mock.call(parentID)]
self.assertEqual(self.session.getTag.call_args_list, expected_calls)
self.session.groupListRemove.assert_not_called()
@mock.patch('sys.stdout', new_callable=six.StringIO)
@ -77,14 +130,16 @@ class TestRemoveGroup(utils.CliTestCase):
@mock.patch('koji_cli.commands.activate_session')
def test_handle_remove_group(self, activate_session_mock, stdout, stderr):
tag = 'tag'
tagID = 100
group = 'group'
arguments = [tag, group]
# Mock out the xmlrpc server
self.session.hasPerm.return_value = True
self.session.getTag.return_value = tag
self.session.getTag.return_value = {'name': tag, 'id': tagID}
self.session.getTagGroups.return_value = [
{'name': 'group', 'group_id': 'groupId'}]
{'name': 'group', 'group_id': 'groupId', 'blocked': False, 'tag_id': tagID}]
# so case where group is present and directly in the tag
rv = handle_remove_group(self.options, self.session, arguments)
@ -92,7 +147,7 @@ class TestRemoveGroup(utils.CliTestCase):
activate_session_mock.assert_called_once_with(self.session, self.options)
self.session.hasPerm.assert_called_once_with('admin')
self.session.getTag.assert_called_once_with(tag)
self.session.getTagGroups.assert_called_once_with(tag, inherit=False)
self.session.getTagGroups.assert_called_once_with(tagID, incl_pkgs=False, incl_reqs=False, incl_blocked=True)
self.session.groupListRemove.assert_called_once_with(tag, group)
self.assertEqual(rv, None)

View file

@ -0,0 +1,195 @@
from __future__ import absolute_import
try:
from unittest import mock
except ImportError:
import mock
import six
from koji_cli.commands import handle_remove_group_pkg
import koji
from . import utils
class TestRemoveGroupPkg(utils.CliTestCase):
def setUp(self):
# Show long diffs in error output...
self.maxDiff = None
self.options = mock.MagicMock()
self.options.debug = False
self.session = mock.MagicMock()
self.session.getAPIVersion.return_value = koji.API_VERSION
self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start()
self.error_format = """Usage: %s remove-group-pkg [options] <tag> <group> <pkg> [<pkg> ...]
(Specify the --help global option for a list of other help options)
%s: error: {message}
""" % (self.progname, self.progname)
def tearDown(self):
mock.patch.stopall()
def test_handle_remove_pkg_not_existing_tag(self):
tag = 'tag'
package = 'package'
group = 'group'
args = [tag, group, package]
self.session.getTag.return_value = None
self.assert_system_exit(
handle_remove_group_pkg,
self.options, self.session, args,
stderr='No such tag: %s\n' % tag,
stdout='',
activate_session=None,
exit_code=1)
# Finally, assert that things were called as we expected.
self.activate_session_mock.assert_called_once_with(self.session, self.options)
self.session.getTag.assert_called_once_with(tag)
self.session.groupPackageListRemove.assert_not_called()
self.session.multiCall.assert_not_called()
def test_handle_remove_pkg_wrong_count_args(self):
tag = 'tag'
group = 'group'
args = [tag, group]
expected_error = self.format_error_message(
'You must specify a tag name, group name, and one or more package names')
self.assert_system_exit(
handle_remove_group_pkg,
self.options, self.session, args,
stderr=expected_error,
stdout='',
activate_session=None,
exit_code=2)
# Finally, assert that things were called as we expected.
self.activate_session_mock.assert_not_called()
self.session.getTag.assert_not_called()
self.session.groupPackageListRemove.assert_not_called()
self.session.multiCall.assert_not_called()
@mock.patch('sys.stdout', new_callable=six.StringIO)
@mock.patch('sys.stderr', new_callable=six.StringIO)
def test_handle_remove_pkg(self, stderr, stdout):
tag = 'tag'
tagID = 1
dsttag = {'name': tag, 'id': tagID}
package = 'package'
group = 'group'
args = [tag, group, package]
self.session.getTag.return_value = dsttag
self.session.getTagGroups.return_value = [
{'name': group, 'group_id': 'groupId', 'blocked': False, 'tag_id': tagID,
'packagelist': [{'package': package, 'blocked': False, 'tag_id': tagID}]
}]
self.session.groupPackageListRemove.return_value = None
handle_remove_group_pkg(self.options, self.session, args)
actual = stderr.getvalue()
expected = ''
self.assertMultiLineEqual(actual, expected)
actual = stdout.getvalue()
expected = ''
self.assertMultiLineEqual(actual, expected)
# Finally, assert that things were called as we expected.
self.activate_session_mock.assert_called_once_with(self.session, self.options)
self.session.getTag.assert_called_once_with(tag)
def test_handle_group_not_present(self):
tag = 'tag'
tagID = 1
dsttag = {'name': tag, 'id': tagID}
package = 'package'
group = 'group'
args = [tag, group, package]
self.session.getTag.return_value = dsttag
self.session.getTagGroups.return_value = []
self.session.groupPackageListRemove.return_value = None
self.assert_system_exit(
handle_remove_group_pkg,
self.options, self.session, args,
stderr='Group group is not present in tag tag\n',
stdout='',
exit_code=1)
self.session.getTag.assert_called_once_with(tag)
def test_handle_package_not_included(self):
tag = 'tag'
tagID = 1
dsttag = {'name': tag, 'id': tagID}
package = 'package'
group = 'group'
args = [tag, group, package]
self.session.getTag.return_value = dsttag
self.session.getTagGroups.return_value = [
{'name': group, 'group_id': 'groupId', 'blocked': False, 'tag_id': tagID,
'packagelist': []
}]
self.session.groupPackageListRemove.return_value = None
self.assert_system_exit(
handle_remove_group_pkg,
self.options, self.session, args,
stdout='Package package is not included in this group\n',
stderr='Invalid parameters\n',
exit_code=1)
self.session.getTag.assert_called_once_with(tag)
def test_handle_package_blocked(self):
tag = 'tag'
tagID = 1
dsttag = {'name': tag, 'id': tagID}
package = 'package'
group = 'group'
args = [tag, group, package]
self.session.getTag.return_value = dsttag
self.session.getTagGroups.return_value = [
{'name': group, 'group_id': 'groupId', 'blocked': False, 'tag_id': tagID,
'packagelist': [{'package': package, 'blocked': True, 'tag_id': tagID}]
}]
self.session.groupPackageListRemove.return_value = None
self.assert_system_exit(
handle_remove_group_pkg,
self.options, self.session, args,
stdout='Package package is blocked in this group. '
'You could use unblock-group-pkg to unblock it\n',
stderr='Invalid parameters\n',
exit_code=1)
self.session.getTag.assert_called_once_with(tag)
def test_handle_package_inherited(self):
tag = 'tag'
tagID = 1
dsttag = {'name': tag, 'id': tagID}
parentID = 20
ptag = {'name': 'parent', 'id': parentID}
package = 'package'
group = 'group'
args = [tag, group, package]
self.session.getTag.side_effect = [dsttag, ptag]
self.session.getTagGroups.return_value = [
{'name': group, 'group_id': 'groupId', 'blocked': False, 'tag_id': tagID,
'packagelist': [{'package': package, 'blocked': False, 'tag_id': parentID}]
}]
self.session.groupPackageListRemove.return_value = None
self.assert_system_exit(
handle_remove_group_pkg,
self.options, self.session, args,
stdout='The entry for package package is inherited from parent. You could use block-group-pkg to prevent this\n',
stderr='Invalid parameters\n',
exit_code=1)
# the end

View file

@ -0,0 +1,201 @@
from __future__ import absolute_import
try:
from unittest import mock
except ImportError:
import mock
import six
from koji_cli.commands import handle_remove_group_req
import koji
from . import utils
class TestRemoveGroupReq(utils.CliTestCase):
def setUp(self):
# Show long diffs in error output...
self.maxDiff = None
self.options = mock.MagicMock()
self.options.debug = False
self.session = mock.MagicMock()
self.session.getAPIVersion.return_value = koji.API_VERSION
self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start()
self.error_format = """Usage: %s remove-group-req [options] <tag> <group> <req> [<req> ...]
(Specify the --help global option for a list of other help options)
%s: error: {message}
""" % (self.progname, self.progname)
def tearDown(self):
mock.patch.stopall()
def test_wrong_count_args(self):
tag = 'tag'
group = 'group'
args = [tag, group]
expected_error = self.format_error_message(
'You must specify a tag name, group name, and one or more requirement names')
self.assert_system_exit(
handle_remove_group_req,
self.options, self.session, args,
stderr=expected_error,
stdout='',
activate_session=None,
exit_code=2)
# Finally, assert that things were called as we expected.
self.activate_session_mock.assert_not_called()
self.session.getTag.assert_not_called()
self.session.groupReqListRemove.assert_not_called()
self.session.multicall.assert_not_called()
def test_no_such_tag(self):
tag = 'tag'
group = 'group'
req = 'other_group'
args = [tag, group, req]
self.session.getTag.return_value = None
self.assert_system_exit(
handle_remove_group_req,
self.options, self.session, args,
stderr='No such tag: tag\n',
stdout='',
activate_session=None,
exit_code=1)
# Finally, assert that things were called as we expected.
self.session.getTag.assert_called_once()
self.session.groupReqListRemove.assert_not_called()
self.session.multicall.assert_not_called()
@mock.patch('sys.stdout', new_callable=six.StringIO)
@mock.patch('sys.stderr', new_callable=six.StringIO)
def test_success(self, stderr, stdout):
tag = 'tag'
tagID = 1
dsttag = {'name': tag, 'id': tagID}
group = 'group'
req = 'other_group'
args = [tag, group, req]
self.session.getTag.return_value = dsttag
self.session.getTagGroups.return_value = [
{'name': group, 'group_id': 'groupId', 'blocked': False, 'tag_id': tagID,
'grouplist': [{'name': req, 'blocked': False, 'tag_id': tagID}]
}]
handle_remove_group_req(self.options, self.session, args)
actual = stderr.getvalue()
expected = ''
self.assertMultiLineEqual(actual, expected)
actual = stdout.getvalue()
expected = ''
self.assertMultiLineEqual(actual, expected)
# Finally, assert that things were called as we expected.
self.activate_session_mock.assert_called_once_with(self.session, self.options)
self.session.getTag.assert_called_once_with(tag)
self.session.multicall.assert_called_once()
def test_handle_group_not_present(self):
tag = 'tag'
tagID = 1
dsttag = {'name': tag, 'id': tagID}
group = 'group'
req = 'other_group'
args = [tag, group, req]
self.session.getTag.return_value = dsttag
self.session.getTagGroups.return_value = []
self.assert_system_exit(
handle_remove_group_req,
self.options, self.session, args,
stderr='Group group is not present in tag tag\n',
stdout='',
exit_code=1)
self.session.getTag.assert_called_once_with(tag)
self.session.groupReqListRemove.assert_not_called()
self.session.multicall.assert_not_called()
def test_handle_req_not_included(self):
tag = 'tag'
tagID = 1
dsttag = {'name': tag, 'id': tagID}
group = 'group'
req = 'other_group'
args = [tag, group, req]
self.session.getTag.return_value = dsttag
self.session.getTagGroups.return_value = [
{'name': group, 'group_id': 'groupId', 'blocked': False, 'tag_id': tagID,
'grouplist': []
}]
self.assert_system_exit(
handle_remove_group_req,
self.options, self.session, args,
stdout='Req other_group is not included in this group\n',
stderr='Invalid parameters\n',
exit_code=1)
self.session.getTag.assert_called_once_with(tag)
self.session.groupReqListRemove.assert_not_called()
self.session.multicall.assert_not_called()
def test_handle_req_blocked(self):
tag = 'tag'
tagID = 1
dsttag = {'name': tag, 'id': tagID}
group = 'group'
req = 'other_group'
args = [tag, group, req]
self.session.getTag.return_value = dsttag
self.session.getTagGroups.return_value = [
{'name': group, 'group_id': 'groupId', 'blocked': False, 'tag_id': tagID,
'grouplist': [{'name': req, 'blocked': True, 'tag_id': tagID}]
}]
self.assert_system_exit(
handle_remove_group_req,
self.options, self.session, args,
stdout='Req other_group is blocked in this group. '
'You could use unblock-group-req to unblock it\n',
stderr='Invalid parameters\n',
exit_code=1)
self.session.getTag.assert_called_once_with(tag)
self.session.groupReqListRemove.assert_not_called()
self.session.multicall.assert_not_called()
def test_handle_req_inherited(self):
tag = 'tag'
tagID = 1
dsttag = {'name': tag, 'id': tagID}
parentID = 20
ptag = {'name': 'parent', 'id': parentID}
group = 'group'
req = 'other_group'
args = [tag, group, req]
self.session.getTag.side_effect = [dsttag, ptag]
self.session.getTagGroups.return_value = [
{'name': group, 'group_id': 'groupId', 'blocked': False, 'tag_id': tagID,
'grouplist': [{'name': req, 'blocked': False, 'tag_id': parentID}]
}]
self.assert_system_exit(
handle_remove_group_req,
self.options, self.session, args,
stdout='The entry for req other_group is inherited from parent. You could use block-group-req to prevent this\n',
stderr='Invalid parameters\n',
exit_code=1)
self.session.groupReqListRemove.assert_not_called()
self.session.multicall.assert_not_called()
# the end

View file

@ -0,0 +1,74 @@
from __future__ import absolute_import
try:
from unittest import mock
except ImportError:
import mock
import six
import koji
from koji_cli.commands import handle_unblock_group
from . import utils
class TestBlockGroup(utils.CliTestCase):
# Show long diffs in error output...
maxDiff = None
def setUp(self):
self.maxDiff = None
self.options = mock.MagicMock()
self.options.debug = False
self.session = mock.MagicMock()
self.session.getAPIVersion.return_value = koji.API_VERSION
self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start()
self.error_format = """Usage: %s unblock-group [options] <tag> <group>
(Specify the --help global option for a list of other help options)
%s: error: {message}
""" % (self.progname, self.progname)
def tearDown(self):
mock.patch.stopall()
def test_handle_unblock_group_badargs(self):
tag = 'tag'
group = 'group'
arguments = [tag, group, 'extra_arg']
self.session.hasPerm.return_value = True
self.session.getTag.return_value = None
expected = self.format_error_message(
"You must specify a tag name and group name")
# Run it and check immediate output
self.assert_system_exit(
handle_unblock_group,
self.options, self.session, arguments,
stderr=expected,
stdout='',
activate_session=None,
exit_code=2)
# Finally, assert that things were called as we expected.
self.activate_session_mock.assert_not_called()
self.session.groupListUnblock.assert_not_called()
@mock.patch('sys.stdout', new_callable=six.StringIO)
def test_handle_unblock_group(self, stdout):
tag = 'tag'
group = 'group'
arguments = [tag, group]
# Run it and check immediate output
rv = handle_unblock_group(self.options, self.session, arguments)
actual = stdout.getvalue()
expected = ''
self.assertMultiLineEqual(actual, expected)
# Finally, assert that things were called as we expected.
self.activate_session_mock.assert_called_once_with(self.session, self.options)
self.session.groupListUnblock.assert_called_once_with(tag, group)
self.assertEqual(rv, None)
# the end