From 4c8a92cb939551769788ff8199b4f227ee4d369c Mon Sep 17 00:00:00 2001 From: Mariana Ulaieva Date: Wed, 20 Nov 2019 14:27:51 +0100 Subject: [PATCH] Integration test for the Cancel And Resume Implement the integration test for the Cancel and Resume scenario. --- tests/integration/conftest.py | 3 - tests/integration/example.test.env.yaml | 5 +- tests/integration/test_failed_build.py | 8 +- tests/integration/test_normal_build.py | 2 +- .../test_resume_cancelled_build.py | 33 ++++++ tests/integration/test_scratch_build.py | 2 +- tests/integration/utils.py | 101 +++++++++++++++++- tox.ini | 2 +- 8 files changed, 140 insertions(+), 16 deletions(-) create mode 100644 tests/integration/test_resume_cancelled_build.py diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 943b3347..813b916a 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -48,9 +48,6 @@ def repo(request, test_env): args = [ "--branch", repo_conf["branch"], - "--single-branch", - "--depth", - "1", url, tempdir, ] diff --git a/tests/integration/example.test.env.yaml b/tests/integration/example.test.env.yaml index aa11a5c4..69eb5cb0 100644 --- a/tests/integration/example.test.env.yaml +++ b/tests/integration/example.test.env.yaml @@ -22,7 +22,7 @@ testdata: module: testmodule # Branch which is going to be built for this test. branch: scratch-build-branch -failed_build: + failed_build: module: testmodule branch: failed-build-branch # Batch considered by this test. @@ -40,3 +40,6 @@ failed_build: buildorder: [{"module-build-macros"}, {"attr"}, {"acl"}] # True if buildrequire a Platform stream representing a GA RHEL release platform_is_ga: true + resume_cancelled_build: + module: testmodule + branch: cancel-build-branch diff --git a/tests/integration/test_failed_build.py b/tests/integration/test_failed_build.py index a9c41f75..422c44bc 100644 --- a/tests/integration/test_failed_build.py +++ b/tests/integration/test_failed_build.py @@ -6,7 +6,7 @@ import utils def test_failed_build(test_env, repo, koji): """ - Run a scratch build with "rebuild_strategy=all". + Run the build with "rebuild_strategy=all". Check that: * Check that the module build eventually fails @@ -26,8 +26,8 @@ def test_failed_build(test_env, repo, koji): batch = test_env["testdata"]["failed_build"]["batch"] failing_components = test_env["testdata"]["failed_build"]["failing_components"] canceled_components = test_env["testdata"]["failed_build"]["canceled_components"] - assert sorted(failing_components) == sorted(build.components(state="FAILED", batch=batch)) + assert sorted(failing_components) == sorted(build.component_names(state="FAILED", batch=batch)) assert sorted(canceled_components) == sorted( - build.components(state="COMPLETE", batch=batch) - + build.components(state="CANCELED", batch=batch) + build.component_names(state="COMPLETE", batch=batch) + + build.component_names(state="CANCELED", batch=batch) ) diff --git a/tests/integration/test_normal_build.py b/tests/integration/test_normal_build.py index a35eb98e..a1ca5d66 100644 --- a/tests/integration/test_normal_build.py +++ b/tests/integration/test_normal_build.py @@ -25,7 +25,7 @@ def test_normal_build(test_env, repo, koji): "rebuild_strategy=all", reuse=test_env["testdata"]["normal_build"].get("build_id"), ) - assert sorted(build.components()) == sorted(repo.components + ["module-build-macros"]) + assert sorted(build.component_names()) == sorted(repo.components + ["module-build-macros"]) expected_buildorder = test_env["testdata"]["normal_build"]["buildorder"] expected_buildorder = [set(batch) for batch in expected_buildorder] diff --git a/tests/integration/test_resume_cancelled_build.py b/tests/integration/test_resume_cancelled_build.py new file mode 100644 index 00000000..e27ff15e --- /dev/null +++ b/tests/integration/test_resume_cancelled_build.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT + +import utils +import time + + +def test_resume_cancelled_build(test_env, repo, koji): + """ + Run the build with "rebuild_strategy=all". + Wait until the module-build-macros build is submitted to Koji. + Cancel module build. + Resume the module with "rhpkg-stage module-build -w". + + Check that: + * Check that the testmodule had actually been cancelled + * Check that the testmodule build succeeded + + """ + build = utils.Build(test_env["packaging_utility"], test_env["mbs_api"]) + repo.bump() + build.run( + "--optional", + "rebuild_strategy=all", + ) + build.wait_for_koji_task_id(package="module-build-macros", batch=1) + build.cancel() + # Behave like a human: restarting the build too quickly would lead to an error. + time.sleep(10) + build.run("--watch") + + assert build.state_name == "ready" + assert build.was_cancelled() diff --git a/tests/integration/test_scratch_build.py b/tests/integration/test_scratch_build.py index b53d56ef..0636a85e 100644 --- a/tests/integration/test_scratch_build.py +++ b/tests/integration/test_scratch_build.py @@ -24,7 +24,7 @@ def test_scratch_build(test_env, repo, koji): ) assert build.state_name == "done" - assert sorted(build.components(state="COMPLETE")) == sorted( + assert sorted(build.component_names(state="COMPLETE")) == sorted( repo.components + ["module-build-macros"] ) diff --git a/tests/integration/utils.py b/tests/integration/utils.py index e53f9b67..cd8c239b 100644 --- a/tests/integration/utils.py +++ b/tests/integration/utils.py @@ -2,12 +2,13 @@ # SPDX-License-Identifier: MIT import re +import time from kobo import rpmlib import koji import yaml import requests -from sh import Command +from sh import Command, git class Koji: @@ -99,6 +100,16 @@ class Repo: elif self._version == 2: return self._modulemd["data"]["dependencies"][0]["buildrequires"].get("platform") + def bump(self): + """Create a "bump" commit""" + args = [ + "--allow-empty", + "-m", + "Bump" + ] + git("commit", *args) + git("push") + class Build: """Wrapper class to work with module builds @@ -108,6 +119,7 @@ class Build: :attribute string _mbs_api: URL of the MBS API (including trailing '/') :attribute string _url: URL of this MBS module build :attribute string _data: Module build data cache for this build fetched from MBS + :attribute string _module_build_data: Verbose module build data cache for this build """ def __init__(self, packaging_utility, mbs_api): @@ -116,6 +128,7 @@ class Build: self._data = None self._component_data = None self._build_id = None + self._module_build_data = None def run(self, *args, reuse=None): """Run a module build @@ -135,6 +148,16 @@ class Build: self._build_id = int(re.search(self._mbs_api + r"module-builds/(\d+)", stdout).group(1)) return self._build_id + def cancel(self): + """Cancel the module build + + :return: Standard output of the "module-build-cancel command + :rtype: str + """ + stdout = self._packaging_utility("module-build-cancel", self._build_id).stdout.decode( + "utf-8") + return stdout + @property def data(self): """Module build data cache for this build fetched from MBS""" @@ -157,26 +180,57 @@ class Build: self._component_data = r.json() return self._component_data + @property + def module_build_data(self): + """Verbose module build + + :return: Dictionary of the verbose module build parameters + :rtype: dict + """ + if self._build_id: + params = { + "verbose": True, + } + r = requests.get(f"{self._mbs_api}module-builds/{self._build_id}", params=params) + r.raise_for_status() + self._module_build_data = r.json() + return self._module_build_data + @property def state_name(self): """Name of the state of this module build""" return self.data["state_name"] - def components(self, state="COMPLETE", batch=None): - """Components of this module build which are in some state and in some batch + def components(self, state=None, batch=None, package=None): + """Components of this module build, optionally filtered based on properties :param string state: Koji build state the components should be in :param int batch: the number of the batch the components should be in + :param string package: name of the component (package) :return: List of filtered components - :rtype: list of strings + :rtype: list of dict """ filtered = self.component_data["items"] if batch is not None: filtered = filter(lambda x: x["batch"] == batch, filtered) if state is not None: filtered = filter(lambda x: x["state_name"] == state, filtered) + if package is not None: + filtered = filter(lambda x: x["package"] == package, filtered) - return [item["package"] for item in filtered] + return list(filtered) + + def component_names(self, state=None, batch=None, package=None): + """Component names of this module build, optionally filtered based on properties + + :param string state: Koji build state the components should be in + :param int batch: the number of the batch the components should be in + :param string: name of component (package): + :return: List of components packages + :rtype: list of strings + """ + components = self.components(state, batch, package) + return [item["package"] for item in components] def batches(self): """ @@ -195,6 +249,28 @@ class Build: return batches + def wait_for_koji_task_id(self, package, batch, timeout=60, sleep=10): + """Wait until the component is submitted to Koji (has a task_id) + + :param string: name of component (package) + :param int batch: the number of the batch the components should be in + :param int timeout: time in seconds + :param int sleep: time in seconds + """ + start = time.time() + while time.time() - start <= timeout: + # Clear cached data + self._component_data = None + components = self.components(package=package, batch=batch) + # Wait until the right component appears and has a task_id + if components and components[0]["task_id"]: + return components[0]["task_id"] + time.sleep(sleep) + + raise RuntimeError( + f'Koji task for "{package}" did not start in {timeout} seconds' + ) + def nvr(self, name_suffix=""): """NVR dictionary of this module build @@ -207,3 +283,18 @@ class Build: "version": self.data["stream"].replace("-", "_"), "release": f'{self.data["version"]}.{self.data["context"]}', } + + def was_cancelled(self): + """Checking in the status trace if module was canceled + + :return: Whether exists required status + :rtype: bool + """ + for item in self.module_build_data["state_trace"]: + if ( + item["reason"] is not None + and "Canceled" in item["reason"] + and item["state_name"] == "failed" + ): + return True + return False diff --git a/tox.ini b/tox.ini index 09e9e8ab..deab2549 100644 --- a/tox.ini +++ b/tox.ini @@ -79,6 +79,6 @@ deps = sh # Set this to /etc/pki/tls/certs/ca-bundle.crt, for example, # if the instance tested has a self-signed certificate. -passenv = REQUESTS_CA_BUNDLE MBS_TEST_CONFIG MBS_TEST_WORKERS +passenv = REQUESTS_CA_BUNDLE MBS_TEST_CONFIG MBS_TEST_WORKERS HOME commands = pytest -vv --confcutdir=tests/integration -n {env:MBS_TEST_WORKERS:0} {posargs:tests/integration}