From f639b99a7159ada94511622374e9beed4af3f0ea Mon Sep 17 00:00:00 2001 From: Tomas Kopecek Date: Oct 30 2017 12:20:37 +0000 Subject: more runroot tests Related: https://pagure.io/koji/issue/641 --- diff --git a/plugins/builder/runroot.py b/plugins/builder/runroot.py index 216a09d..373e880 100644 --- a/plugins/builder/runroot.py +++ b/plugins/builder/runroot.py @@ -123,7 +123,7 @@ class RunRootTask(koji.tasks.BaseTaskHandler): if not tag_arches: raise koji.BuildError("No arch list for tag: %s" % root) #index canonical host arches - host_arches = dict([(koji.canonArch(a), 1) for a in host_arches.split()]) + host_arches = set([koji.canonArch(a) for a in host_arches.split()]) #pick the first suitable match from tag's archlist for br_arch in tag_arches.split(): br_arch = koji.canonArch(br_arch) @@ -192,11 +192,11 @@ class RunRootTask(koji.tasks.BaseTaskHandler): mock_cmd.append('--') mock_cmd.extend(cmdargs) rv = broot.mock(mock_cmd) - log_paths = ['/builddir/runroot.log'] + log_paths = ['builddir/runroot.log'] if upload_logs is not None: log_paths += upload_logs for log_path in log_paths: - self.uploadFile(rootdir + log_path) + self.uploadFile(os.path.join(rootdir, log_path)) finally: # mock should umount its mounts, but it will not handle ours self.undo_mounts(rootdir, fatal=False) @@ -229,8 +229,8 @@ class RunRootTask(koji.tasks.BaseTaskHandler): if mount.find('/../') != -1: raise koji.GenericError("read-write mount point is not safe: %s" % mount) - for re, sub in self.config['path_subs']: - mount = mount.replace(re, sub) + for rep, sub in self.config['path_subs']: + mount = mount.replace(rep, sub) mnts.append(self._get_path_params(mount, rw=True)) self.do_mounts(rootdir, mnts) @@ -280,17 +280,16 @@ class RunRootTask(koji.tasks.BaseTaskHandler): def undo_mounts(self, rootdir, fatal=True): self.logger.debug("Unmounting runroot mounts") - mounts = {} + mounts = set() fn = '%s/tmp/runroot_mounts' % rootdir if os.path.exists(fn): fslog = open(fn, 'r') for line in fslog.readlines(): - mounts.setdefault(line.strip(), 1) + mounts.add(line.strip()) fslog.close() #also, check /proc/mounts just in case - for dir in scan_mounts(rootdir): - mounts.setdefault(dir, 1) - mounts = sorted(mounts.keys()) + mounts |= set(scan_mounts(rootdir)) + mounts = sorted(mounts) # deeper directories first mounts.reverse() failed = [] diff --git a/tests/test_plugins/test_runroot_builder.py b/tests/test_plugins/test_runroot_builder.py index e22af32..21ffc12 100644 --- a/tests/test_plugins/test_runroot_builder.py +++ b/tests/test_plugins/test_runroot_builder.py @@ -10,7 +10,7 @@ import __main__ __main__.BuildRoot = kojid.BuildRoot import koji -from runroot import RunRootTask +import runroot CONFIG1 = { @@ -94,7 +94,7 @@ class TestRunrootConfig(unittest.TestCase): options = mock.MagicMock() options.workdir = '/tmp/nonexistentdirectory' with self.assertRaises(koji.GenericError) as cm: - RunRootTask(123, 'runroot', {}, session, options) + runroot.RunRootTask(123, 'runroot', {}, session, options) self.assertEqual(cm.exception.message, "bad config: missing options in path0 section") @@ -107,7 +107,7 @@ class TestRunrootConfig(unittest.TestCase): options = mock.MagicMock() options.workdir = '/tmp/nonexistentdirectory' with self.assertRaises(koji.GenericError) as cm: - RunRootTask(123, 'runroot', {}, session, options) + runroot.RunRootTask(123, 'runroot', {}, session, options) self.assertEqual(cm.exception.message, "bad config: all paths (default_mounts, safe_roots, path_subs) needs to be absolute: ") @@ -117,7 +117,7 @@ class TestRunrootConfig(unittest.TestCase): session = mock.MagicMock() options = mock.MagicMock() options.workdir = '/tmp/nonexistentdirectory' - RunRootTask(123, 'runroot', {}, session, options) + runroot.RunRootTask(123, 'runroot', {}, session, options) @mock.patch('ConfigParser.SafeConfigParser') def test_valid_config_alt(self, safe_config_parser): @@ -125,7 +125,7 @@ class TestRunrootConfig(unittest.TestCase): session = mock.MagicMock() options = mock.MagicMock() options.workdir = '/tmp/nonexistentdirectory' - RunRootTask(123, 'runroot', {}, session, options) + runroot.RunRootTask(123, 'runroot', {}, session, options) @mock.patch('ConfigParser.SafeConfigParser') def test_pathnum_gaps(self, safe_config_parser): @@ -134,7 +134,7 @@ class TestRunrootConfig(unittest.TestCase): options.workdir = '/tmp/nonexistentdirectory' config = CONFIG2.copy() safe_config_parser.return_value = FakeConfigParser(config) - task1 = RunRootTask(123, 'runroot', {}, session, options) + task1 = runroot.RunRootTask(123, 'runroot', {}, session, options) # adjust the path numbers (but preserving order) and do it again config = CONFIG2.copy() config['path99'] = config['path1'] @@ -142,7 +142,7 @@ class TestRunrootConfig(unittest.TestCase): del config['path1'] del config['path2'] safe_config_parser.return_value = FakeConfigParser(config) - task2 = RunRootTask(123, 'runroot', {}, session, options) + task2 = runroot.RunRootTask(123, 'runroot', {}, session, options) # resulting processed config should be the same self.assertEqual(task1.config, task2.config) paths = list([CONFIG2[k] for k in ('path0', 'path1', 'path2')]) @@ -157,7 +157,7 @@ class TestRunrootConfig(unittest.TestCase): config['paths']['path_subs'] += 'incorrect:format' safe_config_parser.return_value = FakeConfigParser(config) with self.assertRaises(koji.GenericError): - RunRootTask(123, 'runroot', {}, session, options) + runroot.RunRootTask(123, 'runroot', {}, session, options) class TestMounts(unittest.TestCase): @@ -167,7 +167,7 @@ class TestMounts(unittest.TestCase): self.session = mock.MagicMock() options = mock.MagicMock() options.workdir = '/tmp/nonexistentdirectory' - self.t = RunRootTask(123, 'runroot', {}, self.session, options) + self.t = runroot.RunRootTask(123, 'runroot', {}, self.session, options) def test_get_path_params(self): # non-existent item @@ -252,11 +252,40 @@ class TestMounts(unittest.TestCase): self.assertEqual(cm.exception.message, "bad config: background mount not allowed") + def test_do_extra_mounts(self): + self.t.do_mounts = mock.MagicMock() + self.t._get_path_params = mock.MagicMock() + self.t._get_path_params.return_value = 'path_params' + + # no mounts + self.t.do_extra_mounts('rootdir', []) + self.t.do_mounts.assert_called_once_with('rootdir', []) + + # safe mount + self.t.do_mounts.reset_mock() + self.t.do_extra_mounts('rootdir', ['/mnt/workdir/tmp/xyz']) + self.t.do_mounts.assert_called_once_with('rootdir', ['path_params']) + + # unsafe mount + self.t.do_mounts.reset_mock() + with self.assertRaises(koji.GenericError): + self.t.do_extra_mounts('rootdir', ['unsafe']) + self.t.do_mounts.assert_not_called() + + # hackish mount + self.t.do_mounts.reset_mock() + with self.assertRaises(koji.GenericError): + self.t.do_extra_mounts('rootdir', ['/mnt/workdir/tmp/../xyz']) + self.t.do_mounts.assert_not_called() + + + @mock.patch('runroot.scan_mounts') @mock.patch('os.unlink') @mock.patch('commands.getstatusoutput') @mock.patch('os.path.exists') - def test_undo_mounts(self, path_exists, getstatusoutput, os_unlink): + def test_undo_mounts(self, path_exists, getstatusoutput, os_unlink, scan_mounts): self.t.logger = mock.MagicMock() + scan_mounts.return_value = ['mount_1', 'mount_2'] # correct getstatusoutput.return_value = (0, 'ok') @@ -265,7 +294,7 @@ class TestMounts(unittest.TestCase): self.t.undo_mounts('rootdir') self.t.logger.assert_has_calls([ mock.call.debug('Unmounting runroot mounts'), - mock.call.info("Unmounting (runroot): ['mountpoint']"), + mock.call.info("Unmounting (runroot): ['mountpoint', 'mount_2', 'mount_1']"), ]) os_unlink.assert_called_once_with('rootdir/tmp/runroot_mounts') @@ -276,9 +305,83 @@ class TestMounts(unittest.TestCase): with mock.patch('runroot.open', mock.mock_open(read_data = 'mountpoint')): with self.assertRaises(koji.GenericError) as cm: self.t.undo_mounts('rootdir') - self.assertEqual(cm.exception.message, 'Unable to unmount: mountpoint: error') + self.assertEqual(cm.exception.message, + 'Unable to unmount: mountpoint: error, mount_2: error, mount_1: error') + os_unlink.assert_not_called() class TestHandler(unittest.TestCase): - # TODO - pass + @mock.patch('ConfigParser.SafeConfigParser') + def setUp(self, safe_config_parser): + self.session = mock.MagicMock() + self.br = mock.MagicMock() + self.br.mock.return_value = 0 + self.br.id = 678 + self.br.rootdir.return_value = '/rootdir' + runroot.BuildRoot = mock.MagicMock() + runroot.BuildRoot.return_value = self.br + + options = mock.MagicMock() + options.workdir = '/tmp/nonexistentdirectory' + #options.topurl = 'http://topurl' + options.topurls = None + self.t = runroot.RunRootTask(123, 'runroot', {}, self.session, options) + self.t.config['default_mounts'] = ['default_mount'] + self.t.do_mounts = mock.MagicMock() + self.t.do_extra_mounts = mock.MagicMock() + self.t.undo_mounts = mock.MagicMock() + self.t.uploadFile = mock.MagicMock() + self.t._get_path_params = mock.MagicMock() + self.t._get_path_params.side_effect = lambda x: x + + def tearDown(self): + runroot.BuildRoot = kojid.BuildRoot + + @mock.patch('platform.uname') + @mock.patch('os.system') + def test_handler_simple(self, os_system, platform_uname): + platform_uname.return_value = ('system', 'node', 'release', 'version', 'machine', 'arch') + self.session.getBuildConfig.return_value = { + 'id': 456, + 'name': 'tag_name', + 'arches': 'noarch x86_64', + 'extra': {}, + } + self.session.repoInfo.return_value = { + 'id': 1, + 'create_event': 123, + 'state': koji.REPO_STATES['READY'], + 'tag_id': 456, + 'tag_name': 'tag_name', + } + self.session.getRepo.return_value = { + 'id': 1, + } + self.session.host.getHost.return_value = {'arches': 'x86_64'} + self.t.handler('tag_name', 'noarch', 'command', weight=10.0, + repo_id=1, packages=['rpm_a', 'rpm_b'], new_chroot=True, + mounts=['/mnt/a'], skip_setarch=True, + upload_logs=['log_1', 'log_2']) + + # calls + self.session.host.setTaskWeight.assert_called_once_with(self.t.id, 10.0) + self.session.host.getHost.assert_called_once_with() + self.session.getBuildConfig.assert_called_once_with('tag_name') + self.session.repoInfo.assert_called_once_with(1, strict=True) + self.session.host.subtask.assert_not_called() + runroot.BuildRoot.assert_called_once_with(self.session, self.t.options, + 'tag_name', 'x86_64', self.t.id, repo_id=1, setup_dns=True) + os_system.assert_called_once() + self.session.host.setBuildRootState.assert_called_once_with(678, 'BUILDING') + self.br.mock.assert_has_calls([ + mock.call(['--install', 'rpm_a', 'rpm_b']), + mock.call(['chroot', '--new-chroot', '--arch', 'arch', '--', '/bin/sh', '-c', '{ command; } < /dev/null 2>&1 | /usr/bin/tee /builddir/runroot.log; exit ${PIPESTATUS[0]}']), + ]) + self.session.host.updateBuildRootList.assert_called_once_with(678, self.br.getPackageList()) + self.t.do_mounts.assert_called_once_with('/rootdir', ['default_mount']) + self.t.do_extra_mounts.assert_called_once_with('/rootdir', ['/mnt/a']) + self.t.uploadFile.assert_has_calls([ + mock.call('/rootdir/builddir/runroot.log'), + mock.call('/rootdir/log_1'), + mock.call('/rootdir/log_2'), + ]) diff --git a/tests/test_plugins/test_runroot_cli.py b/tests/test_plugins/test_runroot_cli.py index 7005e63..9f57833 100644 --- a/tests/test_plugins/test_runroot_cli.py +++ b/tests/test_plugins/test_runroot_cli.py @@ -20,7 +20,19 @@ class TestListCommands(unittest.TestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.args = ['TAG', 'ARCH', 'COMMAND'] + self.args = [ + '--skip-setarch', + '--use-shell', + '--new-chroot', + '--task-id', + '--package', 'rpm_a', + '--package', 'rpm_b', + '--mount', 'mount_a', + '--mount', 'mount_b', + '--weight', '3', + '--channel-override', 'some_channel', + '--repo-id', '12345', + 'TAG', 'ARCH', 'COMMAND'] self.old_OptionParser = runroot.OptionParser runroot.OptionParser = mock.MagicMock(side_effect=self.get_parser) self.old_watch_tasks = runroot.watch_tasks @@ -39,19 +51,21 @@ class TestListCommands(unittest.TestCase): # Show long diffs in error output... maxDiff = None + @mock.patch('time.sleep') @mock.patch('sys.stdout', new_callable=six.StringIO) - def test_handle_runroot(self, stdout): + def test_handle_runroot(self, stdout, sleep): # Mock out the xmlrpc server self.session.getTaskInfo.return_value = {'state': 1} self.session.downloadTaskOutput.return_value = 'task output' self.session.listTaskOutput.return_value = {'runroot.log': ['DEFAULT']} self.session.runroot.return_value = 1 + self.session.taskFinished.side_effect = [False, True] # Run it and check immediate output runroot.handle_runroot(self.options, self.session, self.args) actual = stdout.getvalue() actual = actual.replace('nosetests', 'koji') - expected = 'task output' + expected = '1\ntask output' self.assertMultiLineEqual(actual, expected) # Finally, assert that things were called as we expected. @@ -60,9 +74,13 @@ class TestListCommands(unittest.TestCase): self.session.downloadTaskOutput.assert_called_once_with( 1, 'runroot.log', volume='DEFAULT') self.session.runroot.assert_called_once_with( - 'TAG', 'ARCH', ['COMMAND'], repo_id=mock.ANY, weight=mock.ANY, - mounts=mock.ANY, packages=mock.ANY, skip_setarch=mock.ANY, - channel=mock.ANY, + 'TAG', 'ARCH', 'COMMAND', + repo_id=12345, weight=3, + mounts=['mount_a', 'mount_b'], + packages=['rpm_a', 'rpm_b'], + skip_setarch=True, + channel='some_channel', + new_chroot=True, ) def test_handle_runroot_watch(self): @@ -109,3 +127,27 @@ class TestListCommands(unittest.TestCase): self.session.listTaskOutput.assert_not_called() self.session.downloadTaskOutput.assert_not_called() self.session.runroot.assert_called_once() + + def test_fail_call(self): + args = ['--nowait', 'TAG', 'ARCH', 'COMMAND'] + + # Mock out the xmlrpc server + self.session.runroot.side_effect = koji.GenericError() + + with self.assertRaises(koji.GenericError): + runroot.handle_runroot(self.options, self.session, args) + + @mock.patch('sys.stdout', new_callable=six.StringIO) + def test_missing_plugin(self, stdout): + args = ['--nowait', 'TAG', 'ARCH', 'COMMAND'] + + # Mock out the xmlrpc server + self.session.runroot.side_effect = koji.GenericError('Invalid method') + + with self.assertRaises(koji.GenericError): + runroot.handle_runroot(self.options, self.session, args) + + actual = stdout.getvalue().strip() + self.assertEqual(actual, + "* The runroot plugin appears to not be installed on the" + " koji hub. Please contact the administrator.")