From c44c5285cd93a6d045562d45654f9b6d36567949 Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: Apr 16 2026 21:42:31 +0000
Subject: [PATCH 1/5] unit test fixes for rhel8 compat


---

diff --git a/devtools/check-api b/devtools/check-api
index d185358..a85973a 100755
--- a/devtools/check-api
+++ b/devtools/check-api
@@ -9,7 +9,7 @@ from collections import OrderedDict
 
 # import koji code from our checkout
 if os.path.exists(__file__):
-    kojitop = os.path.dirname(os.path.dirname(__file__))
+    kojitop = os.path.normpath(os.path.dirname(__file__) + '/..')
 else:
     # e.g. <stdin>
     kojitop = os.getcwd()
diff --git a/tests/test_cli/test_import_cg.py b/tests/test_cli/test_import_cg.py
index b229e10..f163de3 100644
--- a/tests/test_cli/test_import_cg.py
+++ b/tests/test_cli/test_import_cg.py
@@ -21,7 +21,8 @@ class TestImportCG(utils.CliTestCase):
         self.session = mock.MagicMock()
         self.workdir = tempfile.mkdtemp()
         self.outdir = self.workdir + '/output'
-        self.unique_path_mock = mock.patch('koji_cli.commands.unique_path').start()
+        self.unique_path_mock = mock.patch('koji_cli.commands.unique_path',
+                                           return_value='cli-import/12345.foobar').start()
         self.running_in_bg = mock.patch('koji_cli.commands._running_in_bg').start()
         self.running_in_bg.return_value = False
         self.linked_upload = mock.patch('koji_cli.commands.linked_upload').start()
@@ -82,10 +83,10 @@ class TestImportCG(utils.CliTestCase):
         handle_import_cg(self.options, self.session, arguments)
 
         self.assertEqual(len(self.session.uploadWrapper.mock_calls), len(metadata['output']))
-        kwargs = self.session.uploadWrapper.call_args.kwargs
+        kwargs = self.session.uploadWrapper.call_args[1]
         self.assertEqual(kwargs['callback'], _progress_callback)
         self.session.CGImport.assert_called_once()
-        args = self.session.CGImport.call_args.args
+        args = self.session.CGImport.call_args[0]
         self.assertEqual(args[0], metadata)
         self.linked_upload.assert_not_called()
 
@@ -172,7 +173,7 @@ class TestImportCG(utils.CliTestCase):
         self.session.uploadWrapper.assert_not_called()
         self.linked_upload.assert_not_called()
         self.session.CGImport.assert_called_once()
-        args = self.session.CGImport.call_args.args
+        args = self.session.CGImport.call_args[0]
         self.assertEqual(args[0], metadata)
 
     def test_handle_import_cg_draft(self):
@@ -189,7 +190,7 @@ class TestImportCG(utils.CliTestCase):
 
         self.assertEqual(len(self.session.uploadWrapper.mock_calls), len(metadata['output']))
         self.session.CGImport.assert_called_once()
-        args = self.session.CGImport.call_args.args
+        args = self.session.CGImport.call_args[0]
         self.assertEqual(args[0], metadata)
 
     def test_handle_import_cg_reserve(self):
@@ -206,7 +207,7 @@ class TestImportCG(utils.CliTestCase):
 
         self.assertEqual(len(self.session.uploadWrapper.mock_calls), len(metadata['output']))
         self.session.CGImport.assert_called_once()
-        args = self.session.CGImport.call_args.args
+        args = self.session.CGImport.call_args[0]
         self.assertEqual(args[0], expect)
 
     def test_handle_import_cg_linked(self):
@@ -220,7 +221,7 @@ class TestImportCG(utils.CliTestCase):
         self.session.uploadWrapper.assert_not_called()
         self.assertEqual(len(self.linked_upload.mock_calls), len(metadata['output']))
         self.session.CGImport.assert_called_once()
-        args = self.session.CGImport.call_args.args
+        args = self.session.CGImport.call_args[0]
         self.assertEqual(args[0], metadata)
 
     def test_handle_import_cg_noprogress(self):
@@ -232,10 +233,10 @@ class TestImportCG(utils.CliTestCase):
         handle_import_cg(self.options, self.session, arguments)
 
         self.assertEqual(len(self.session.uploadWrapper.mock_calls), len(metadata['output']))
-        kwargs = self.session.uploadWrapper.call_args.kwargs
+        kwargs = self.session.uploadWrapper.call_args[1]
         self.assertEqual(kwargs['callback'], None)
         self.session.CGImport.assert_called_once()
-        args = self.session.CGImport.call_args.args
+        args = self.session.CGImport.call_args[0]
         self.assertEqual(args[0], metadata)
         self.linked_upload.assert_not_called()
 
diff --git a/tests/test_hub/test_repo_requests.py b/tests/test_hub/test_repo_requests.py
index 3192af6..3f9562d 100644
--- a/tests/test_hub/test_repo_requests.py
+++ b/tests/test_hub/test_repo_requests.py
@@ -599,8 +599,8 @@ class TestQueueTask(BaseTest):
         repos.repo_queue_task(req)
 
         self.make_task.assert_called_once()
-        method, args = self.make_task.call_args.args
-        taskopts = self.make_task.call_args.kwargs
+        method, args = self.make_task.call_args[0]
+        taskopts = self.make_task.call_args[1]
         self.assertEqual(method, 'newRepo')
         self.assertEqual(taskopts['channel'], 'createrepo')
         params = koji.tasks.parse_task_params('newRepo', args)
@@ -633,7 +633,7 @@ class TestUpdateEndEvents(BaseTest):
         self.tag_last_change_event.assert_not_called()
         expect = [{'id': 1, 'end_event': 1001}]
         self.BulkUpdateProcessor.assert_called_once()
-        updates = self.BulkUpdateProcessor.call_args.kwargs['data']
+        updates = self.BulkUpdateProcessor.call_args[1]['data']
         self.assertEqual(updates, expect)
 
     def test_event_cache(self):
@@ -662,7 +662,7 @@ class TestUpdateEndEvents(BaseTest):
         self.assertEqual(self.tag_first_change_event.mock_calls, expect_calls)
         expect_updates = [{'id': 4, 'end_event': 1000}]
         self.BulkUpdateProcessor.assert_called_once()
-        updates = self.BulkUpdateProcessor.call_args.kwargs['data']
+        updates = self.BulkUpdateProcessor.call_args[1]['data']
         self.assertEqual(updates, expect_updates)
 
 
@@ -799,14 +799,14 @@ class TestGetRepo(BaseTest):
         repos.get_repo('TAGID', at_event=101010)
 
         self.RepoQuery.assert_called_once()
-        clauses, fields, qopts = self.RepoQuery.call_args.args
+        clauses, fields, qopts = self.RepoQuery.call_args[0]
         self.assertIn(['create_event', '=', 101010], clauses)
 
     def test_get_repo_min_event(self):
         repos.get_repo('TAGID', min_event=101010)
 
         self.RepoQuery.assert_called_once()
-        clauses, fields, qopts = self.RepoQuery.call_args.args
+        clauses, fields, qopts = self.RepoQuery.call_args[0]
         self.assertIn(['create_event', '>=', 101010], clauses)
 
 
@@ -996,12 +996,12 @@ class TestRequestRepo(BaseTest):
 
         # check all the calls made with the value
         self.get_repo.assert_called_once()
-        ev = self.get_repo.call_args.kwargs['min_event']
+        ev = self.get_repo.call_args[1]['min_event']
         self.assertEqual(ev, last)
         clauses = self.RepoQueueQuery.call_args_list[0].args[0]
         self.assertIn(['min_event', '>=', last], clauses)
         self.InsertProcessor.assert_called_once()
-        data = self.InsertProcessor.call_args.kwargs['data']
+        data = self.InsertProcessor.call_args[1]['data']
         self.assertEqual(data['min_event'], last)
 
     def test_request_priority(self):
@@ -1015,7 +1015,7 @@ class TestRequestRepo(BaseTest):
 
         # check all the calls made with the value
         self.InsertProcessor.assert_called_once()
-        data = self.InsertProcessor.call_args.kwargs['data']
+        data = self.InsertProcessor.call_args[1]['data']
         self.assertEqual(data['min_event'], ev + 10)  # tag last change
         self.assertEqual(data['priority'], 25)  # default + 5
 
@@ -1106,12 +1106,12 @@ class TestRequestRepo(BaseTest):
 
         # check all the calls made with the value
         self.get_repo.assert_called_once()
-        ev = self.get_repo.call_args.kwargs['min_event']
+        ev = self.get_repo.call_args[1]['min_event']
         self.assertEqual(ev, last)
         clauses = self.RepoQueueQuery.call_args_list[0].args[0]
         self.assertIn(['min_event', '>=', last], clauses)
         self.InsertProcessor.assert_called_once()
-        data = self.InsertProcessor.call_args.kwargs['data']
+        data = self.InsertProcessor.call_args[1]['data']
         self.assertEqual(data['min_event'], last)
 
 
@@ -1253,7 +1253,7 @@ class TestDefaultMinEvent(BaseTest):
         # we should report the ts from the last event, minus lag
         self.assertEqual(ev, 9999)
         self.getLastEvent.assert_called_once()
-        base_ts = self.getLastEvent.call_args.kwargs['before']
+        base_ts = self.getLastEvent.call_args[1]['before']
         self.assertEqual(base_ts, now - 3600)
 
     def test_tag_older(self):
@@ -1269,7 +1269,7 @@ class TestDefaultMinEvent(BaseTest):
         # we should report the ts for the tag, since it is older
         self.assertEqual(ev, 9900)
         self.getLastEvent.assert_called_once()
-        base_ts = self.getLastEvent.call_args.kwargs['before']
+        base_ts = self.getLastEvent.call_args[1]['before']
         self.assertEqual(base_ts, now - 3600)
 
     def test_lag_override(self):
@@ -1285,7 +1285,7 @@ class TestDefaultMinEvent(BaseTest):
         # we should report the ts from the last event, minus lag
         self.assertEqual(ev, 9999)
         self.getLastEvent.assert_called_once()
-        base_ts = self.getLastEvent.call_args.kwargs['before']
+        base_ts = self.getLastEvent.call_args[1]['before']
         self.assertEqual(base_ts, now - 1800)
 
     def test_lag_override_invalid(self):
@@ -1301,7 +1301,7 @@ class TestDefaultMinEvent(BaseTest):
         # we should report the ts from the last event, minus lag
         self.assertEqual(ev, 9999)
         self.getLastEvent.assert_called_once()
-        base_ts = self.getLastEvent.call_args.kwargs['before']
+        base_ts = self.getLastEvent.call_args[1]['before']
         self.assertEqual(base_ts, now - 3600)
 
     def test_no_last_event(self):
@@ -1405,32 +1405,32 @@ class TestExports(BaseTest):
         with self.assertRaises(koji.ActionNotAllowed):
             self.exports.setRequestPriority(99, 1)
         self.set_request_priority.assert_not_called()
-        self.assertEqual(self.assertPerm.call_args.args, ('admin',))
+        self.assertEqual(self.assertPerm.call_args[0], ('admin',))
 
         with self.assertRaises(koji.ActionNotAllowed):
             self.exports.setState(99, 1)
         self.repo_set_state.assert_not_called()
-        self.assertEqual(self.assertPerm.call_args.args, ('repo',))
+        self.assertEqual(self.assertPerm.call_args[0], ('repo',))
 
         with self.assertRaises(koji.ActionNotAllowed):
             self.exports.setExternalRepoData(99, 1)
         self.set_external_repo_data.assert_not_called()
-        self.assertEqual(self.assertPerm.call_args.args, ('repo',))
+        self.assertEqual(self.assertPerm.call_args[0], ('repo',))
 
         with self.assertRaises(koji.ActionNotAllowed):
             self.exports.autoRequests()
         self.do_auto_requests.assert_not_called()
-        self.assertEqual(self.assertPerm.call_args.args, ('repo',))
+        self.assertEqual(self.assertPerm.call_args[0], ('repo',))
 
         with self.assertRaises(koji.ActionNotAllowed):
             self.exports.checkQueue()
         self.check_repo_queue.assert_not_called()
-        self.assertEqual(self.assertPerm.call_args.args, ('repo',))
+        self.assertEqual(self.assertPerm.call_args[0], ('repo',))
 
         with self.assertRaises(koji.ActionNotAllowed):
             self.exports.updateEndEvents()
         self.update_end_events.assert_not_called()
-        self.assertEqual(self.assertPerm.call_args.args, ('repo',))
+        self.assertEqual(self.assertPerm.call_args[0], ('repo',))
 
     def test_with_perm(self):
         # assertPerm does not raise
diff --git a/tests/test_hub/test_wrapper_rpm.py b/tests/test_hub/test_wrapper_rpm.py
index d3e2937..0c782f7 100644
--- a/tests/test_hub/test_wrapper_rpm.py
+++ b/tests/test_hub/test_wrapper_rpm.py
@@ -102,7 +102,7 @@ class TestWrapperRPM(unittest.TestCase):
         self.exports.wrapperRPM(self.build, self.url, self.target, opts=opts)
 
         self.make_task.assert_called_once()
-        taskargs = self.make_task.call_args.args[1]
+        taskargs = self.make_task.call_args[0][1]
         expect_opts = {'scratch': True}  # repo_id filtered out
         self.assertEqual(taskargs[4], expect_opts)
 
diff --git a/tests/test_lib/test_utils.py b/tests/test_lib/test_utils.py
index 18fdcf1..6d3ae4c 100644
--- a/tests/test_lib/test_utils.py
+++ b/tests/test_lib/test_utils.py
@@ -1521,7 +1521,7 @@ class TestRmtree(unittest.TestCase):
         self.assertEqual(rmtree_nofork.call_args[0][0], path)
         _exit.assert_called_once()
         if mock.__package__ == 'unittest':
-            logger = rmtree_nofork.call_args.kwargs['logger']
+            logger = rmtree_nofork.call_args[1]['logger']
         else:
             logger = rmtree_nofork.call_args[1]['logger']
 

From e56791db39c71695d57ceb70afa2faa7bca7da45 Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: Apr 16 2026 21:42:31 +0000
Subject: [PATCH 2/5] adjust pathinfo mocking


---

diff --git a/tests/test_hub/test_import_image_internal.py b/tests/test_hub/test_import_image_internal.py
index ec1c6c3..61ca234 100644
--- a/tests/test_hub/test_import_image_internal.py
+++ b/tests/test_hub/test_import_image_internal.py
@@ -4,6 +4,7 @@ import shutil
 import tempfile
 import unittest
 
+import koji
 import kojihub
 
 
@@ -16,9 +17,9 @@ class TestImportImageInternal(unittest.TestCase):
         self.Task.return_value.assertHost = mock.MagicMock()
         self.get_build = mock.patch('kojihub.kojihub.get_build').start()
         self.get_archive_type = mock.patch('kojihub.kojihub.get_archive_type').start()
-        self.path_work = mock.patch('koji.pathinfo.work').start()
+        self.pathinfo = koji.PathInfo(self.tempdir)
+        mock.patch('koji.pathinfo', new=self.pathinfo).start()
         self.import_archive = mock.patch('kojihub.kojihub.import_archive').start()
-        self.build = mock.patch('koji.pathinfo.build').start()
         self.get_rpm = mock.patch('kojihub.kojihub.get_rpm').start()
 
     def tearDown(self):
@@ -45,8 +46,7 @@ class TestImportImageInternal(unittest.TestCase):
             'release': 'release',
         }
         self.get_archive_type.return_value = 4
-        self.path_work.return_value = self.tempdir
-        os.makedirs(self.tempdir + "/tasks/1/1")
+        os.makedirs(self.tempdir + "/work/tasks/1/1")
         kojihub.importImageInternal(
             task_id=1, build_info=self.get_build.return_value, imgdata=imgdata)
 
@@ -85,13 +85,11 @@ class TestImportImageInternal(unittest.TestCase):
         self.get_build.return_value = build_info
         self.get_rpm.return_value = rpm
         self.get_archive_type.return_value = 4
-        self.path_work.return_value = self.tempdir
-        self.build.return_value = self.tempdir
         self.import_archive.return_value = {
             'id': 9,
             'filename': self.tempdir + '/foo.archive',
         }
-        workdir = self.tempdir + "/tasks/1/1"
+        workdir = self.tempdir + "/work/tasks/1/1"
         os.makedirs(workdir)
         # Create a log file to exercise that code path
         with open(workdir + '/foo.log', 'w'):
@@ -102,7 +100,7 @@ class TestImportImageInternal(unittest.TestCase):
         # Check that the log symlink made it to where it was supposed to.
         dest = os.readlink(workdir + '/foo.log')
         dest = os.path.abspath(os.path.join(workdir, dest))
-        self.assertEqual(dest, self.tempdir + '/data/logs/image/foo.log')
+        self.assertEqual(dest, self.tempdir + '/packages/name/version/release/data/logs/image/foo.log')
 
         # And.. check all the sql statements
         self.assertEqual(len(cursor.execute.mock_calls), 1)
@@ -135,13 +133,11 @@ class TestImportImageInternal(unittest.TestCase):
         self.context_db.session.host_id = 42
         self.get_build.return_value = build_info
         self.get_archive_type.return_value = 4
-        self.path_work.return_value = self.tempdir
-        self.build.return_value = self.tempdir
         self.import_archive.return_value = {
             'id': 9,
             'filename': self.tempdir + '/foo.archive',
         }
-        workdir = self.tempdir + "/tasks/1/1"
+        workdir = self.tempdir + "/work/tasks/1/1"
         os.makedirs(workdir)
         # Create a log file to exercise that code path
         with open(workdir + '/foo.log', 'w'):
@@ -149,8 +145,8 @@ class TestImportImageInternal(unittest.TestCase):
 
         # Create a same named log file already present
         # This should force the function to add arch to the final path
-        os.makedirs(self.tempdir + '/data/logs/image')
-        with open(self.tempdir + '/data/logs/image/foo.log', 'w'):
+        os.makedirs(self.tempdir + '/packages/name/version/release/data/logs/image')
+        with open(self.tempdir + '/packages/name/version/release/data/logs/image/foo.log', 'w'):
             pass
 
         kojihub.importImageInternal(task_id=1, build_info=build_info, imgdata=imgdata)
@@ -158,7 +154,7 @@ class TestImportImageInternal(unittest.TestCase):
         # Check that the log symlink made it to where it was supposed to.
         dest = os.readlink(workdir + '/foo.log')
         dest = os.path.abspath(os.path.join(workdir, dest))
-        self.assertEqual(dest, self.tempdir + '/data/logs/image/x86_64/foo.log')
+        self.assertEqual(dest, self.tempdir + '/packages/name/version/release/data/logs/image/x86_64/foo.log')
 
     def test_with_livemedia_task(self):
         taskinfo = {'id': 101010, 'method': 'livemedia'}
@@ -182,13 +178,11 @@ class TestImportImageInternal(unittest.TestCase):
         self.context_db.session.host_id = 42
         self.get_build.return_value = build_info
         self.get_archive_type.return_value = 4
-        self.path_work.return_value = self.tempdir
-        self.build.return_value = self.tempdir
         self.import_archive.return_value = {
             'id': 9,
             'filename': self.tempdir + '/foo.archive',
         }
-        workdir = self.tempdir + "/tasks/1/1"
+        workdir = self.tempdir + "/work/tasks/1/1"
         os.makedirs(workdir)
         # Create a log file to exercise that code path
         with open(workdir + '/foo.log', 'w'):
@@ -199,7 +193,7 @@ class TestImportImageInternal(unittest.TestCase):
         # Check that the log symlink made it to where it was supposed to.
         dest = os.readlink(workdir + '/foo.log')
         dest = os.path.abspath(os.path.join(workdir, dest))
-        self.assertEqual(dest, self.tempdir + '/data/logs/image/x86_64/foo.log')
+        self.assertEqual(dest, self.tempdir + '/packages/name/version/release/data/logs/image/x86_64/foo.log')
 
 
 # the end

From b7bc522f5909a95e1bfcfd2d730923afd4b586f7 Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: Apr 16 2026 21:42:31 +0000
Subject: [PATCH 3/5] more compat fix for mocks


---

diff --git a/tests/test_hub/test_repo_requests.py b/tests/test_hub/test_repo_requests.py
index 3f9562d..fa21b5f 100644
--- a/tests/test_hub/test_repo_requests.py
+++ b/tests/test_hub/test_repo_requests.py
@@ -998,7 +998,7 @@ class TestRequestRepo(BaseTest):
         self.get_repo.assert_called_once()
         ev = self.get_repo.call_args[1]['min_event']
         self.assertEqual(ev, last)
-        clauses = self.RepoQueueQuery.call_args_list[0].args[0]
+        clauses = self.RepoQueueQuery.call_args_list[0][0][0]
         self.assertIn(['min_event', '>=', last], clauses)
         self.InsertProcessor.assert_called_once()
         data = self.InsertProcessor.call_args[1]['data']
@@ -1108,7 +1108,7 @@ class TestRequestRepo(BaseTest):
         self.get_repo.assert_called_once()
         ev = self.get_repo.call_args[1]['min_event']
         self.assertEqual(ev, last)
-        clauses = self.RepoQueueQuery.call_args_list[0].args[0]
+        clauses = self.RepoQueueQuery.call_args_list[0][0][0]
         self.assertIn(['min_event', '>=', last], clauses)
         self.InsertProcessor.assert_called_once()
         data = self.InsertProcessor.call_args[1]['data']

From 654c519799c2a0f44f33f31e59632ce9f71e8964 Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: Apr 16 2026 21:42:31 +0000
Subject: [PATCH 4/5] path.join doesn't like mocks on el8


---

diff --git a/tests/test_plugins/test_kiwi_builder.py b/tests/test_plugins/test_kiwi_builder.py
index 7c7b4fc..0a0c7d2 100644
--- a/tests/test_plugins/test_kiwi_builder.py
+++ b/tests/test_plugins/test_kiwi_builder.py
@@ -19,6 +19,7 @@ class TestKiwiBuildTask(unittest.TestCase):
     def SCM(self, *args, **kwargs):
         scm = mock.MagicMock()
         scm.assert_allowed = mock.MagicMock()
+        scm.checkout.return_value = '/tmp/NO-SUCH-PATH'
         self.scm.append(scm)
         return scm
 

From 796d54fdb7395788e3a8f10f2aba3d69828e8d9c Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: Apr 16 2026 21:42:31 +0000
Subject: [PATCH 5/5] skip api test on older pythons


---

diff --git a/tests/test_api/test_api.py b/tests/test_api/test_api.py
index ba561b6..c4d8777 100644
--- a/tests/test_api/test_api.py
+++ b/tests/test_api/test_api.py
@@ -1,4 +1,5 @@
 import os.path
+import sys
 import subprocess
 import unittest
 
@@ -11,6 +12,8 @@ class TestAPICompat(unittest.TestCase):
     # base api data, as generated by dump-api
     BASE = os.path.relpath(os.path.dirname(__file__) + '/data/api.json')
 
+    # this test can be sensitive to wide python version gaps
+    @unittest.skipIf(sys.version_info[:2] < (3, 9), "Skipped on legacy platforms")
     def test_api_compat(self):
         cmd = [self.SCRIPT, self.BASE]
         proc = subprocess.Popen(cmd, stderr=subprocess.PIPE)

