more runroot tests

Related: https://pagure.io/koji/issue/641
This commit is contained in:
Tomas Kopecek 2017-10-30 13:20:37 +01:00
parent dec0c7b77f
commit f639b99a71
3 changed files with 174 additions and 30 deletions

View file

@ -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 = []

View file

@ -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'),
])

View file

@ -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.")