From 477b43d4e9cac604891e68d700fd30b4ed773aa5 Mon Sep 17 00:00:00 2001 From: Haibo Lin Date: Mon, 13 Apr 2020 14:50:00 +0800 Subject: [PATCH] Split repoclosure into separate phase Move repoclosure out from test phase into its own phase and run parallel with image building phases(osbs, imagebuild, ...) to speed things up. JIRA: RHELCMP-8 Signed-off-by: Haibo Lin --- doc/_static/phases.png | Bin 24785 -> 24287 bytes doc/_static/phases.svg | 124 +++++++++++------ doc/phases.rst | 14 +- pungi/phases/__init__.py | 1 + pungi/phases/repoclosure.py | 141 +++++++++++++++++++ pungi/phases/test.py | 117 +--------------- pungi/scripts/pungi_koji.py | 2 + tests/test_repoclosure_phase.py | 236 ++++++++++++++++++++++++++++++++ tests/test_test_phase.py | 218 +---------------------------- 9 files changed, 473 insertions(+), 380 deletions(-) create mode 100644 pungi/phases/repoclosure.py create mode 100644 tests/test_repoclosure_phase.py diff --git a/doc/_static/phases.png b/doc/_static/phases.png index 003808f7f0481b54384f1084c4929b24e69b7d9d..f0564b1a379b9287631c2d464f56c536f74cab72 100644 GIT binary patch literal 24287 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 diff --git a/doc/_static/phases.svg b/doc/_static/phases.svg index 5d5397c8..013f4f40 100644 --- a/doc/_static/phases.svg +++ b/doc/_static/phases.svg @@ -11,12 +11,12 @@ inkscape:export-xdpi="90" inkscape:export-filename="/home/lsedlar/repos/pungi/doc/_static/phases.png" sodipodi:docname="phases.svg" - inkscape:version="0.92.4 (unknown)" + inkscape:version="0.92.2 5c3e80d, 2017-08-06" version="1.1" id="svg2" - viewBox="0 0 771.66458 221.50019" - height="221.50018" - width="771.66455"> + viewBox="0 0 669.66458 255.18195" + height="255.18195" + width="669.66455"> image/svg+xml - + @@ -77,7 +77,7 @@ inkscape:label="Vrstva 1" inkscape:groupmode="layer" id="layer1" - transform="matrix(1.066667,0,0,1.066667,-2.473231,-910.85239)"> + transform="matrix(1.066667,0,0,1.066667,-78.473216,-910.85239)"> @@ -95,7 +95,7 @@ x="51.554729" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" xml:space="preserve">Buildinstall + style="font-size:13.14789963px;line-height:1.25">Buildinstall @@ -220,7 +220,7 @@ id="tspan3370" x="106.1384" y="923.25934" - style="font-size:13.1479px;line-height:1.25">Gather + style="font-size:13.14789963px;line-height:1.25">Gather ExtraFiles + style="font-size:13.1479px;line-height:1.25">ExtraFiles Createrepo + style="font-size:13.1479px;line-height:1.25">Createrepo OSTree + style="font-size:13.14789963px;line-height:1.25">OSTree + id="g1061"> + transform="translate(-0.959604,-80.817124)"> Createiso LiveImages ImageBuild LiveMedia + id="g204"> OSBS + x="421.48166" + y="1019.2621">OSBS + transform="translate(-29.683562,-17.293167)"> ExtraIsos + style="font-size:13.14789963px;line-height:1.25">ExtraIsos + + + + Repoclosure + + + + Repoclosure diff --git a/doc/phases.rst b/doc/phases.rst index 3307482c..189c3095 100644 --- a/doc/phases.rst +++ b/doc/phases.rst @@ -132,6 +132,13 @@ Creates bootable media that carry an ostree repository as a payload. These images are created by running ``lorax`` with special templates. Again it runs in Koji runroot. +Repoclosure +----------- + +Run ``repoclosure`` on each repository. By default errors are only reported +in the log, the compose will still be considered a success. The actual error +has to be looked up in the compose logs directory. Configuration allows customizing this. + ImageChecksum ------------- @@ -145,12 +152,7 @@ Test This phase is supposed to run some sanity checks on the finished compose. -The first test is to run ``repoclosure`` on each repository. By default errors -are only reported in the log, the compose will still be considered a success. -The actual error has to be looked up in the compose logs directory. -Configuration allows customizing this. - -The other test is to check all images listed the metadata and verify that they +The only test is to check all images listed the metadata and verify that they look sane. For ISO files headers are checked to verify the format is correct, and for bootable media a check is run to verify they have properties that allow booting. diff --git a/pungi/phases/__init__.py b/pungi/phases/__init__.py index eecb223c..d99f8789 100644 --- a/pungi/phases/__init__.py +++ b/pungi/phases/__init__.py @@ -27,6 +27,7 @@ from .createiso import CreateisoPhase # noqa from .extra_isos import ExtraIsosPhase # noqa from .live_images import LiveImagesPhase # noqa from .image_build import ImageBuildPhase # noqa +from .repoclosure import RepoclosurePhase # noqa from .test import TestPhase # noqa from .image_checksum import ImageChecksumPhase # noqa from .livemedia_phase import LiveMediaPhase # noqa diff --git a/pungi/phases/repoclosure.py b/pungi/phases/repoclosure.py new file mode 100644 index 00000000..12492dd0 --- /dev/null +++ b/pungi/phases/repoclosure.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- + + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Library General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . + + +import glob +import os +import shutil + +from kobo.shortcuts import run + +from pungi.wrappers import repoclosure +from pungi.arch import get_valid_arches +from pungi.phases.base import PhaseBase +from pungi.phases.gather import get_lookaside_repos, get_gather_methods +from pungi.util import is_arch_multilib, temp_dir, get_arch_variant_data + + +class RepoclosurePhase(PhaseBase): + name = "repoclosure" + + def run(self): + run_repoclosure(self.compose) + + +def run_repoclosure(compose): + msg = "Running repoclosure" + compose.log_info("[BEGIN] %s" % msg) + + # Variant repos + for arch in compose.get_arches(): + is_multilib = is_arch_multilib(compose.conf, arch) + arches = get_valid_arches(arch, is_multilib) + for variant in compose.get_variants(arch=arch): + if variant.is_empty: + continue + + conf = get_arch_variant_data( + compose.conf, "repoclosure_strictness", arch, variant + ) + if conf and conf[-1] == "off": + continue + + prefix = "%s-repoclosure" % compose.compose_id + lookaside = {} + if variant.parent: + repo_id = "%s-%s.%s" % (prefix, variant.parent.uid, arch) + repo_dir = compose.paths.compose.repository( + arch=arch, variant=variant.parent + ) + lookaside[repo_id] = repo_dir + + repos = {} + repo_id = "%s-%s.%s" % (prefix, variant.uid, arch) + repo_dir = compose.paths.compose.repository(arch=arch, variant=variant) + repos[repo_id] = repo_dir + + for i, lookaside_url in enumerate( + get_lookaside_repos(compose, arch, variant) + ): + lookaside[ + "%s-lookaside-%s.%s-%s" % (compose.compose_id, variant.uid, arch, i) + ] = lookaside_url + + logfile = compose.paths.log.log_file(arch, "repoclosure-%s" % variant) + + try: + _, methods = get_gather_methods(compose, variant) + if methods == "hybrid": + # Using hybrid solver, no repoclosure command is available. + pattern = compose.paths.log.log_file( + arch, "hybrid-depsolver-%s-iter-*" % variant + ) + fus_logs = sorted(glob.glob(pattern)) + repoclosure.extract_from_fus_logs(fus_logs, logfile) + else: + _run_repoclosure_cmd(compose, repos, lookaside, arches, logfile) + except RuntimeError as exc: + if conf and conf[-1] == "fatal": + raise + else: + compose.log_warning( + "Repoclosure failed for %s.%s\n%s" % (variant.uid, arch, exc) + ) + finally: + if methods != "hybrid": + _delete_repoclosure_cache_dirs(compose) + + compose.log_info("[DONE ] %s" % msg) + + +def _delete_repoclosure_cache_dirs(compose): + if "dnf" == compose.conf["repoclosure_backend"]: + from dnf.const import SYSTEM_CACHEDIR + from dnf.util import am_i_root + from dnf.yum.misc import getCacheDir + + if am_i_root(): + top_cache_dir = SYSTEM_CACHEDIR + else: + top_cache_dir = getCacheDir() + else: + from yum.misc import getCacheDir + + top_cache_dir = getCacheDir() + + for name in os.listdir(top_cache_dir): + if name.startswith(compose.compose_id): + cache_path = os.path.join(top_cache_dir, name) + if os.path.isdir(cache_path): + shutil.rmtree(cache_path) + else: + os.remove(cache_path) + + +def _run_repoclosure_cmd(compose, repos, lookaside, arches, logfile): + cmd = repoclosure.get_repoclosure_cmd( + backend=compose.conf["repoclosure_backend"], + repos=repos, + lookaside=lookaside, + arch=arches, + ) + # Use temp working directory directory as workaround for + # https://bugzilla.redhat.com/show_bug.cgi?id=795137 + with temp_dir(prefix="repoclosure_") as tmp_dir: + # Ideally we would want show_cmd=True here to include the + # command in the logfile, but due to a bug in Kobo that would + # cause any error to be printed directly to stderr. + # https://github.com/release-engineering/kobo/pull/26 + run(cmd, logfile=logfile, workdir=tmp_dir, show_cmd=True) diff --git a/pungi/phases/test.py b/pungi/phases/test.py index 1baaf0cb..5d3a483e 100644 --- a/pungi/phases/test.py +++ b/pungi/phases/test.py @@ -14,134 +14,19 @@ # along with this program; if not, see . -import glob import os -import shutil -from kobo.shortcuts import run - -from pungi.wrappers import repoclosure -from pungi.arch import get_valid_arches from pungi.phases.base import PhaseBase -from pungi.phases.gather import get_lookaside_repos, get_gather_methods -from pungi.util import is_arch_multilib, failable, temp_dir, get_arch_variant_data +from pungi.util import failable, get_arch_variant_data class TestPhase(PhaseBase): name = "test" def run(self): - run_repoclosure(self.compose) check_image_sanity(self.compose) -def run_repoclosure(compose): - msg = "Running repoclosure" - compose.log_info("[BEGIN] %s" % msg) - - # Variant repos - for arch in compose.get_arches(): - is_multilib = is_arch_multilib(compose.conf, arch) - arches = get_valid_arches(arch, is_multilib) - for variant in compose.get_variants(arch=arch): - if variant.is_empty: - continue - - conf = get_arch_variant_data( - compose.conf, "repoclosure_strictness", arch, variant - ) - if conf and conf[-1] == "off": - continue - - prefix = "%s-repoclosure" % compose.compose_id - lookaside = {} - if variant.parent: - repo_id = "%s-%s.%s" % (prefix, variant.parent.uid, arch) - repo_dir = compose.paths.compose.repository( - arch=arch, variant=variant.parent - ) - lookaside[repo_id] = repo_dir - - repos = {} - repo_id = "%s-%s.%s" % (prefix, variant.uid, arch) - repo_dir = compose.paths.compose.repository(arch=arch, variant=variant) - repos[repo_id] = repo_dir - - for i, lookaside_url in enumerate( - get_lookaside_repos(compose, arch, variant) - ): - lookaside[ - "%s-lookaside-%s.%s-%s" % (compose.compose_id, variant.uid, arch, i) - ] = lookaside_url - - logfile = compose.paths.log.log_file(arch, "repoclosure-%s" % variant) - - try: - _, methods = get_gather_methods(compose, variant) - if methods == "hybrid": - # Using hybrid solver, no repoclosure command is available. - pattern = compose.paths.log.log_file( - arch, "hybrid-depsolver-%s-iter-*" % variant - ) - fus_logs = sorted(glob.glob(pattern)) - repoclosure.extract_from_fus_logs(fus_logs, logfile) - else: - _run_repoclosure_cmd(compose, repos, lookaside, arches, logfile) - except RuntimeError as exc: - if conf and conf[-1] == "fatal": - raise - else: - compose.log_warning( - "Repoclosure failed for %s.%s\n%s" % (variant.uid, arch, exc) - ) - finally: - if methods != "hybrid": - _delete_repoclosure_cache_dirs(compose) - - compose.log_info("[DONE ] %s" % msg) - - -def _delete_repoclosure_cache_dirs(compose): - if "dnf" == compose.conf["repoclosure_backend"]: - from dnf.const import SYSTEM_CACHEDIR - from dnf.util import am_i_root - from dnf.yum.misc import getCacheDir - - if am_i_root(): - top_cache_dir = SYSTEM_CACHEDIR - else: - top_cache_dir = getCacheDir() - else: - from yum.misc import getCacheDir - - top_cache_dir = getCacheDir() - - for name in os.listdir(top_cache_dir): - if name.startswith(compose.compose_id): - cache_path = os.path.join(top_cache_dir, name) - if os.path.isdir(cache_path): - shutil.rmtree(cache_path) - else: - os.remove(cache_path) - - -def _run_repoclosure_cmd(compose, repos, lookaside, arches, logfile): - cmd = repoclosure.get_repoclosure_cmd( - backend=compose.conf["repoclosure_backend"], - repos=repos, - lookaside=lookaside, - arch=arches, - ) - # Use temp working directory directory as workaround for - # https://bugzilla.redhat.com/show_bug.cgi?id=795137 - with temp_dir(prefix="repoclosure_") as tmp_dir: - # Ideally we would want show_cmd=True here to include the - # command in the logfile, but due to a bug in Kobo that would - # cause any error to be printed directly to stderr. - # https://github.com/release-engineering/kobo/pull/26 - run(cmd, logfile=logfile, workdir=tmp_dir, show_cmd=True) - - def check_image_sanity(compose): """ Go through all images in manifest and make basic sanity tests on them. If diff --git a/pungi/scripts/pungi_koji.py b/pungi/scripts/pungi_koji.py index 7cd1a2cb..7dfdd6b6 100644 --- a/pungi/scripts/pungi_koji.py +++ b/pungi/scripts/pungi_koji.py @@ -349,6 +349,7 @@ def run_compose( image_build_phase = pungi.phases.ImageBuildPhase(compose) osbs_phase = pungi.phases.OSBSPhase(compose) image_checksum_phase = pungi.phases.ImageChecksumPhase(compose) + repoclosure_phase = pungi.phases.RepoclosurePhase(compose) test_phase = pungi.phases.TestPhase(compose) # check if all config options are set @@ -467,6 +468,7 @@ def run_compose( image_build_phase, livemedia_phase, osbs_phase, + repoclosure_phase, ) compose_images_phase = pungi.phases.WeaverPhase(compose, compose_images_schema) compose_images_phase.start() diff --git a/tests/test_repoclosure_phase.py b/tests/test_repoclosure_phase.py new file mode 100644 index 00000000..81e4bb5b --- /dev/null +++ b/tests/test_repoclosure_phase.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- + + +try: + import unittest2 as unittest +except ImportError: + import unittest + +import mock +import six + +import pungi.phases.repoclosure as repoclosure_phase +from tests.helpers import DummyCompose, PungiTestCase, mk_boom + +try: + import dnf # noqa: F401 + + HAS_DNF = True +except ImportError: + HAS_DNF = False + +try: + import yum # noqa: F401 + + HAS_YUM = True +except ImportError: + HAS_YUM = False + + +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.repoclosure.run") + def test_repoclosure_skip_if_disabled(self, mock_run, mock_grc): + compose = DummyCompose( + self.topdir, {"repoclosure_strictness": [("^.*$", {"*": "off"})]} + ) + repoclosure_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.repoclosure.run") + def test_repoclosure_default_backend(self, mock_run, mock_grc): + with mock.patch("six.PY2", new=True): + compose = DummyCompose(self.topdir, {}) + + repoclosure_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.repoclosure.run") + def test_repoclosure_dnf_backend(self, mock_run, mock_grc): + compose = DummyCompose(self.topdir, {"repoclosure_backend": "dnf"}) + repoclosure_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.repoclosure.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]) + + repoclosure_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.repoclosure.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): + repoclosure_phase.run_repoclosure(compose) + + @unittest.skipUnless(HAS_DNF, "DNF is not available") + @mock.patch("pungi.wrappers.repoclosure.get_repoclosure_cmd") + @mock.patch("pungi.phases.repoclosure.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"}), + ], + }, + ) + repoclosure_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.repoclosure._delete_repoclosure_cache_dirs") + @mock.patch("pungi.wrappers.repoclosure.get_repoclosure_cmd") + @mock.patch("pungi.phases.repoclosure.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): + repoclosure_phase.run_repoclosure(compose) diff --git a/tests/test_test_phase.py b/tests/test_test_phase.py index 481e8503..486d8198 100644 --- a/tests/test_test_phase.py +++ b/tests/test_test_phase.py @@ -1,17 +1,10 @@ # -*- coding: utf-8 -*- - -try: - import unittest2 as unittest -except ImportError: - import unittest - import mock import os -import six import pungi.phases.test as test_phase -from tests.helpers import DummyCompose, PungiTestCase, touch, mk_boom +from tests.helpers import DummyCompose, PungiTestCase, touch try: import dnf # noqa: F401 @@ -311,212 +304,3 @@ class TestCheckImageSanity(PungiTestCase): 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)