pungi/tests/test_test_phase.py
Lubomír Sedlář aefe9b186d repoclosure: Parse all fus logs
Originally the list of solvables for fus was growing with each iteration
and nothing was ever removed. That later changed so that fus iterations
are only done on newly added stuff. It's great for performance, but
means that the last log is not a superset of all others.

To get all dependency problems we need to look into all log files, not
just the last one.

JIRA: COMPOSE-3964
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
2019-12-02 11:08:55 +01:00

441 lines
17 KiB
Python

# -*- coding: utf-8 -*-
try:
import unittest2 as unittest
except ImportError:
import unittest
import mock
import os
import six
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
import pungi.phases.test as test_phase
from tests.helpers import DummyCompose, PungiTestCase, touch, mk_boom
try:
import dnf
HAS_DNF = True
except ImportError:
HAS_DNF = False
try:
import yum
HAS_YUM = True
except ImportError:
HAS_YUM = False
PAD = b'\0' * 100
UNBOOTABLE_ISO = (b'\0' * 0x8001) + b'CD001' + PAD
ISO_WITH_MBR = (b'\0' * 0x1fe) + b'\x55\xAA' + (b'\0' * 0x7e01) + b'CD001' + PAD
ISO_WITH_GPT = (b'\0' * 0x200) + b'EFI PART' + (b'\0' * 0x7df9) + b'CD001' + PAD
ISO_WITH_MBR_AND_GPT = (b'\0' * 0x1fe) + b'\x55\xAAEFI PART' + (b'\0' * 0x7df9) + b'CD001' + PAD
ISO_WITH_TORITO = (b'\0' * 0x8001) + b'CD001' + (b'\0' * 0x7fa) + b'\0CD001\1EL TORITO SPECIFICATION' + PAD
class TestCheckImageSanity(PungiTestCase):
def test_missing_file_reports_error(self):
compose = DummyCompose(self.topdir, {})
with self.assertRaises(IOError):
test_phase.check_image_sanity(compose)
def test_missing_file_doesnt_report_if_failable(self):
compose = DummyCompose(self.topdir, {})
compose.image.deliverable = 'iso'
compose.image.can_fail = True
try:
test_phase.check_image_sanity(compose)
except Exception:
self.fail('Failable deliverable must not raise')
def test_correct_iso_does_not_raise(self):
compose = DummyCompose(self.topdir, {})
compose.image.format = 'iso'
compose.image.bootable = False
touch(os.path.join(self.topdir, 'compose', compose.image.path), UNBOOTABLE_ISO)
try:
test_phase.check_image_sanity(compose)
except Exception:
self.fail('Correct unbootable image must not raise')
def test_incorrect_iso_raises(self):
compose = DummyCompose(self.topdir, {})
compose.image.format = 'iso'
compose.image.bootable = False
touch(os.path.join(self.topdir, 'compose', compose.image.path), 'Hey there')
with self.assertRaises(RuntimeError) as ctx:
test_phase.check_image_sanity(compose)
self.assertIn('does not look like an ISO file', str(ctx.exception))
def test_bootable_iso_without_mbr_or_gpt_raises_on_x86_64(self):
compose = DummyCompose(self.topdir, {})
compose.image.arch = 'x86_64'
compose.image.format = 'iso'
compose.image.bootable = True
touch(os.path.join(self.topdir, 'compose', compose.image.path), UNBOOTABLE_ISO)
with self.assertRaises(RuntimeError) as ctx:
test_phase.check_image_sanity(compose)
self.assertIn('is supposed to be bootable, but does not have MBR nor GPT',
str(ctx.exception))
def test_bootable_iso_without_mbr_or_gpt_doesnt_raise_on_arm(self):
compose = DummyCompose(self.topdir, {})
compose.image.arch = 'armhfp'
compose.image.format = 'iso'
compose.image.bootable = True
touch(os.path.join(self.topdir, 'compose', compose.image.path), UNBOOTABLE_ISO)
try:
test_phase.check_image_sanity(compose)
except Exception:
self.fail('Failable deliverable must not raise')
def test_failable_bootable_iso_without_mbr_gpt_doesnt_raise(self):
compose = DummyCompose(self.topdir, {})
compose.image.format = 'iso'
compose.image.bootable = True
compose.image.deliverable = 'iso'
compose.image.can_fail = True
touch(os.path.join(self.topdir, 'compose', compose.image.path), UNBOOTABLE_ISO)
try:
test_phase.check_image_sanity(compose)
except Exception:
self.fail('Failable deliverable must not raise')
def test_bootable_iso_with_mbr_does_not_raise(self):
compose = DummyCompose(self.topdir, {})
compose.image.format = 'iso'
compose.image.bootable = True
touch(os.path.join(self.topdir, 'compose', compose.image.path), ISO_WITH_MBR)
try:
test_phase.check_image_sanity(compose)
except Exception:
self.fail('Bootable image with MBR must not raise')
def test_bootable_iso_with_gpt_does_not_raise(self):
compose = DummyCompose(self.topdir, {})
compose.image.format = 'iso'
compose.image.bootable = True
touch(os.path.join(self.topdir, 'compose', compose.image.path), ISO_WITH_GPT)
try:
test_phase.check_image_sanity(compose)
except Exception:
self.fail('Bootable image with GPT must not raise')
def test_bootable_iso_with_mbr_and_gpt_does_not_raise(self):
compose = DummyCompose(self.topdir, {})
compose.image.format = 'iso'
compose.image.bootable = True
touch(os.path.join(self.topdir, 'compose', compose.image.path), ISO_WITH_MBR_AND_GPT)
try:
test_phase.check_image_sanity(compose)
except Exception:
self.fail('Bootable image with MBR and GPT must not raise')
def test_bootable_iso_with_el_torito_does_not_raise(self):
compose = DummyCompose(self.topdir, {})
compose.image.format = 'iso'
compose.image.bootable = True
touch(os.path.join(self.topdir, 'compose', compose.image.path), ISO_WITH_TORITO)
try:
test_phase.check_image_sanity(compose)
except Exception:
self.fail('Bootable image with El Torito must not raise')
def test_checks_with_optional_variant(self):
compose = DummyCompose(self.topdir, {})
compose.variants['Server'].variants = {
'optional': mock.Mock(uid='Server-optional', arches=['x86_64'],
type='optional', is_empty=False)
}
compose.image.format = 'iso'
compose.image.bootable = True
touch(os.path.join(self.topdir, 'compose', compose.image.path), ISO_WITH_MBR_AND_GPT)
image = mock.Mock(path="Server/i386/optional/iso/image.iso",
format='iso', bootable=False)
compose.im.images['Server-optional'] = {'i386': [image]}
try:
test_phase.check_image_sanity(compose)
except Exception:
self.fail('Checking optional variant must not raise')
@mock.patch("pungi.phases.test.check_sanity", new=mock.Mock())
def test_too_big_iso(self):
compose = DummyCompose(self.topdir, {"createiso_max_size": [(".*", {"*": 10})]})
compose.image.format = 'iso'
compose.image.bootable = False
compose.image.size = 20
test_phase.check_image_sanity(compose)
warnings = [call[0][0] for call in compose.log_warning.call_args_list]
self.assertIn(
"ISO Client/i386/iso/image.iso is too big. Expected max 10B, got 20B",
warnings,
)
@mock.patch("pungi.phases.test.check_sanity", new=mock.Mock())
def test_too_big_iso_strict(self):
compose = DummyCompose(
self.topdir,
{
"createiso_max_size": [(".*", {"*": 10})],
"createiso_max_size_is_strict": [(".*", {"*": True})],
},
)
compose.image.format = 'iso'
compose.image.bootable = False
compose.image.size = 20
with self.assertRaises(RuntimeError) as ctx:
test_phase.check_image_sanity(compose)
self.assertEqual(
str(ctx.exception),
"ISO Client/i386/iso/image.iso is too big. Expected max 10B, got 20B",
)
@mock.patch("pungi.phases.test.check_sanity", new=mock.Mock())
def test_too_big_iso_not_strict(self):
compose = DummyCompose(
self.topdir,
{
"createiso_max_size": [(".*", {"*": 10})],
"createiso_max_size_is_strict": [(".*", {"*": False})],
},
)
compose.image.format = 'iso'
compose.image.bootable = False
compose.image.size = 20
test_phase.check_image_sanity(compose)
warnings = [call[0][0] for call in compose.log_warning.call_args_list]
self.assertIn(
"ISO Client/i386/iso/image.iso is too big. Expected max 10B, got 20B",
warnings,
)
@mock.patch("pungi.phases.test.check_sanity", new=mock.Mock())
def test_too_big_unified(self):
compose = DummyCompose(self.topdir, {})
compose.image.format = 'iso'
compose.image.bootable = False
compose.image.size = 20
compose.image.unified = True
setattr(compose.image, "_max_size", 10)
test_phase.check_image_sanity(compose)
warnings = [call[0][0] for call in compose.log_warning.call_args_list]
self.assertIn(
"ISO Client/i386/iso/image.iso is too big. Expected max 10B, got 20B",
warnings,
)
@mock.patch("pungi.phases.test.check_sanity", new=mock.Mock())
def test_too_big_unified_strict(self):
compose = DummyCompose(
self.topdir,
{"createiso_max_size_is_strict": [(".*", {"*": True})]},
)
compose.image.format = 'iso'
compose.image.bootable = False
compose.image.size = 20
compose.image.unified = True
setattr(compose.image, "_max_size", 10)
with self.assertRaises(RuntimeError) as ctx:
test_phase.check_image_sanity(compose)
self.assertEqual(
str(ctx.exception),
"ISO Client/i386/iso/image.iso is too big. Expected max 10B, got 20B",
)
@mock.patch("pungi.phases.test.check_sanity", new=mock.Mock())
def test_fits_in_limit(self):
compose = DummyCompose(self.topdir, {"createiso_max_size": [(".*", {"*": 20})]})
compose.image.format = 'iso'
compose.image.bootable = False
compose.image.size = 5
test_phase.check_image_sanity(compose)
self.assertEqual(compose.log_warning.call_args_list, [])
@mock.patch("pungi.phases.test.check_sanity", new=mock.Mock())
def test_non_iso(self):
compose = DummyCompose(self.topdir, {"createiso_max_size": [(".*", {"*": 10})]})
compose.image.format = 'qcow2'
compose.image.bootable = False
compose.image.size = 20
test_phase.check_image_sanity(compose)
self.assertEqual(compose.log_warning.call_args_list, [])
class TestRepoclosure(PungiTestCase):
def setUp(self):
super(TestRepoclosure, self).setUp()
self.maxDiff = None
def _get_repo(self, compose_id, variant, arch, path=None):
path = path or arch + '/os'
return {
'%s-repoclosure-%s.%s' % (compose_id, variant, arch): self.topdir + '/compose/%s/%s' % (variant, path)
}
@mock.patch('pungi.wrappers.repoclosure.get_repoclosure_cmd')
@mock.patch('pungi.phases.test.run')
def test_repoclosure_skip_if_disabled(self, mock_run, mock_grc):
compose = DummyCompose(self.topdir, {
'repoclosure_strictness': [('^.*$', {'*': 'off'})]
})
test_phase.run_repoclosure(compose)
self.assertEqual(mock_grc.call_args_list, [])
@unittest.skipUnless(HAS_YUM, 'YUM is not available')
@mock.patch('pungi.wrappers.repoclosure.get_repoclosure_cmd')
@mock.patch('pungi.phases.test.run')
def test_repoclosure_default_backend(self, mock_run, mock_grc):
with mock.patch('six.PY2', new=True):
compose = DummyCompose(self.topdir, {})
test_phase.run_repoclosure(compose)
six.assertCountEqual(
self,
mock_grc.call_args_list,
[mock.call(backend='yum', arch=['amd64', 'x86_64', 'noarch'], lookaside={},
repos=self._get_repo(compose.compose_id, 'Everything', 'amd64')),
mock.call(backend='yum', arch=['amd64', 'x86_64', 'noarch'], lookaside={},
repos=self._get_repo(compose.compose_id, 'Client', 'amd64')),
mock.call(backend='yum', arch=['amd64', 'x86_64', 'noarch'], lookaside={},
repos=self._get_repo(compose.compose_id, 'Server', 'amd64')),
mock.call(backend='yum', arch=['x86_64', 'noarch'], lookaside={},
repos=self._get_repo(compose.compose_id, 'Server', 'x86_64')),
mock.call(backend='yum', arch=['x86_64', 'noarch'], lookaside={},
repos=self._get_repo(compose.compose_id, 'Everything', 'x86_64'))])
@unittest.skipUnless(HAS_DNF, 'DNF is not available')
@mock.patch('pungi.wrappers.repoclosure.get_repoclosure_cmd')
@mock.patch('pungi.phases.test.run')
def test_repoclosure_dnf_backend(self, mock_run, mock_grc):
compose = DummyCompose(self.topdir, {'repoclosure_backend': 'dnf'})
test_phase.run_repoclosure(compose)
six.assertCountEqual(
self,
mock_grc.call_args_list,
[mock.call(backend='dnf', arch=['amd64', 'x86_64', 'noarch'], lookaside={},
repos=self._get_repo(compose.compose_id, 'Everything', 'amd64')),
mock.call(backend='dnf', arch=['amd64', 'x86_64', 'noarch'], lookaside={},
repos=self._get_repo(compose.compose_id, 'Client', 'amd64')),
mock.call(backend='dnf', arch=['amd64', 'x86_64', 'noarch'], lookaside={},
repos=self._get_repo(compose.compose_id, 'Server', 'amd64')),
mock.call(backend='dnf', arch=['x86_64', 'noarch'], lookaside={},
repos=self._get_repo(compose.compose_id, 'Server', 'x86_64')),
mock.call(backend='dnf', arch=['x86_64', 'noarch'], lookaside={},
repos=self._get_repo(compose.compose_id, 'Everything', 'x86_64'))])
@mock.patch("glob.glob")
@mock.patch("pungi.wrappers.repoclosure.extract_from_fus_logs")
@mock.patch("pungi.wrappers.repoclosure.get_repoclosure_cmd")
@mock.patch("pungi.phases.test.run")
def test_repoclosure_hybrid_variant(self, mock_run, mock_grc, effl, glob):
compose = DummyCompose(
self.topdir, {"repoclosure_backend": "dnf", "gather_method": "hybrid"}
)
f = mock.Mock()
glob.return_value = [f]
def _log(a, v):
return compose.paths.log.log_file(a, "repoclosure-%s" % compose.variants[v])
test_phase.run_repoclosure(compose)
self.assertEqual(mock_grc.call_args_list, [])
six.assertCountEqual(
self,
effl.call_args_list,
[
mock.call([f], _log("amd64", "Everything")),
mock.call([f], _log("amd64", "Client")),
mock.call([f], _log("amd64", "Server")),
mock.call([f], _log("x86_64", "Server")),
mock.call([f], _log("x86_64", "Everything")),
]
)
@mock.patch('pungi.wrappers.repoclosure.get_repoclosure_cmd')
@mock.patch('pungi.phases.test.run')
def test_repoclosure_report_error(self, mock_run, mock_grc):
compose = DummyCompose(self.topdir, {
'repoclosure_strictness': [('^.*$', {'*': 'fatal'})]
})
mock_run.side_effect = mk_boom(cls=RuntimeError)
with self.assertRaises(RuntimeError):
test_phase.run_repoclosure(compose)
@unittest.skipUnless(HAS_DNF, 'DNF is not available')
@mock.patch('pungi.wrappers.repoclosure.get_repoclosure_cmd')
@mock.patch('pungi.phases.test.run')
def test_repoclosure_overwrite_options_creates_correct_commands(self, mock_run, mock_grc):
compose = DummyCompose(self.topdir, {
'repoclosure_backend': 'dnf',
'repoclosure_strictness': [
('^.*$', {'*': 'off'}),
('^Server$', {'*': 'fatal'}),
]
})
test_phase.run_repoclosure(compose)
six.assertCountEqual(
self,
mock_grc.call_args_list,
[mock.call(backend='dnf', arch=['amd64', 'x86_64', 'noarch'], lookaside={},
repos=self._get_repo(compose.compose_id, 'Server', 'amd64')),
mock.call(backend='dnf', arch=['x86_64', 'noarch'], lookaside={},
repos=self._get_repo(compose.compose_id, 'Server', 'x86_64')),
])
@mock.patch('pungi.phases.test._delete_repoclosure_cache_dirs')
@mock.patch('pungi.wrappers.repoclosure.get_repoclosure_cmd')
@mock.patch('pungi.phases.test.run')
def test_repoclosure_uses_correct_behaviour(self, mock_run, mock_grc, mock_del):
compose = DummyCompose(self.topdir, {
'repoclosure_backend': 'dnf',
'repoclosure_strictness': [
('^.*$', {'*': 'off'}),
('^Server$', {'*': 'fatal'}),
]
})
mock_run.side_effect = mk_boom(cls=RuntimeError)
with self.assertRaises(RuntimeError):
test_phase.run_repoclosure(compose)