From 3bbdf9848dafd9df12b7af49d8ec41ecd034a54c Mon Sep 17 00:00:00 2001 From: Qixiang Wan Date: Apr 18 2017 06:02:07 +0000 Subject: [PATCH 1/2] Fix consumer's topics are not subscribed freshmaker consumer's topic is an instance attribute, if it is not set prior to call parent's (which is FedmsgConsumer) __init__(), the consumer will not subscribe to its topics while instantiating. --- diff --git a/freshmaker/consumer.py b/freshmaker/consumer.py index a7d454c..1deb528 100644 --- a/freshmaker/consumer.py +++ b/freshmaker/consumer.py @@ -42,10 +42,10 @@ class FreshmakerConsumer(fedmsg.consumers.FedmsgConsumer): config_key = 'freshmakerconsumer' def __init__(self, hub): - super(FreshmakerConsumer, self).__init__(hub) - + # set topic before super, otherwise topic will not be subscribed self.handlers = list(freshmaker.handlers.load_handlers()) self.register_parsers() + super(FreshmakerConsumer, self).__init__(hub) # These two values are typically provided either by the unit tests or # by the local build command. They are empty in the production environ diff --git a/tests/test_consumer.py b/tests/test_consumer.py index fe5ce5a..6be6912 100644 --- a/tests/test_consumer.py +++ b/tests/test_consumer.py @@ -22,19 +22,17 @@ import unittest import mock import fedmsg.config -from mock import patch -from freshmaker.consumer import FreshmakerConsumer +import freshmaker -@patch("freshmaker.consumer.get_global_consumer") -class TestPoller(unittest.TestCase): - +class ConsumerTest(unittest.TestCase): def setUp(self): pass def tearDown(self): pass + @mock.patch("freshmaker.consumer.get_global_consumer") def test_consumer_processing_message(self, global_consumer): """ Tests that consumer parses the message, forwards the event @@ -43,7 +41,7 @@ class TestPoller(unittest.TestCase): """ hub = mock.MagicMock() hub.config = fedmsg.config.load_config() - consumer = FreshmakerConsumer(hub) + consumer = freshmaker.consumer.FreshmakerConsumer(hub) global_consumer.return_value = consumer msg = {'body': { @@ -61,3 +59,21 @@ class TestPoller(unittest.TestCase): event = consumer.incoming.get() self.assertEqual(event.msg_id, "ModuleBuilt handled") + + @mock.patch("freshmaker.consumer.get_global_consumer") + def test_consumer_subscribe_to_specified_topics(self, global_consumer): + """ + Tests consumer will try to subscribe specified topics. + """ + hub = mock.MagicMock() + hub.config = fedmsg.config.load_config() + consumer = freshmaker.consumer.FreshmakerConsumer(hub) + global_consumer.return_value = consumer + topics = freshmaker.events.BaseEvent.get_parsed_topics() + callback = consumer._consume_json if consumer.jsonify else consumer.consume + for topic in topics: + self.assertIn(mock.call(topic, callback), hub.subscribe.call_args_list) + + +if __name__ == '__main__': + unittest.main() From 0dee476614297966dcc096a5f1bae851f54cbc32 Mon Sep 17 00:00:00 2001 From: Qixiang Wan Date: Apr 19 2017 06:04:31 +0000 Subject: [PATCH 2/2] Rebuild depending modules when receives module built message When there is a module built message received, and state for the module built is 'ready', query PDC to get depending modules, send module build request to MBS. For the depending modules, we will filter out any modules that are not the latest release of (module_name, module_version). --- diff --git a/conf/config.py b/conf/config.py index 1e7dd95..cff009e 100644 --- a/conf/config.py +++ b/conf/config.py @@ -39,12 +39,21 @@ class BaseConfiguration(object): # Base URL of git repository with source artifacts. GIT_BASE_URL = "git://pkgs.fedoraproject.org" + # SSH base URL of git repository + GIT_SSH_BASE_URL = "ssh://%s@pkgs.fedoraproject.org/" + + # GIT user for cloning and pushing repo + GIT_USER = "" + # Base URL of Module Build Service. MBS_BASE_URL = "https://mbs.fedoraproject.org" # Authorization token to use when communicating with MBS. MBS_AUTH_TOKEN = "" + # PDC API URL + PDC_URL = 'http://pdc.fedoraproject.org/rest_api/v1' + # Read Koji configuration from profile instead of reading them from # configuration file directly. For staging Koji, it is stg. KOJI_PROFILE = 'koji' diff --git a/freshmaker/config.py b/freshmaker/config.py index 35f16b2..eabb1c0 100644 --- a/freshmaker/config.py +++ b/freshmaker/config.py @@ -139,6 +139,18 @@ class Config(object): 'type': str, 'default': "git://pkgs.fedoraproject.org", 'desc': 'Dist-git base URL.'}, + 'git_ssh_base_url': { + 'type': str, + 'default': "ssh://%s@pkgs.fedoraproject.org/", + 'desc': 'Dist-git ssh base URL.'}, + 'git_user': { + 'type': str, + 'default': '', + 'desc': 'User for git operations.'}, + 'git_author': { + 'type': str, + 'default': 'Freshmaker ', + 'desc': 'Author for git commit.'}, 'mbs_base_url': { 'type': str, 'default': "https://mbs.fedoraproject.org", diff --git a/freshmaker/events.py b/freshmaker/events.py index f91a693..b152769 100644 --- a/freshmaker/events.py +++ b/freshmaker/events.py @@ -129,10 +129,12 @@ class ModuleBuilt(BaseEvent): :param module_build_id: the id of the module build :param module_build_state: the state of the module build """ - def __init__(self, msg_id, module_build_id, module_build_state): + def __init__(self, msg_id, module_build_id, module_build_state, name, stream): super(ModuleBuilt, self).__init__(msg_id) self.module_build_id = module_build_id self.module_build_state = module_build_state + self.module_name = name + self.module_stream = stream class ModuleMetadataUpdated(BaseEvent): diff --git a/freshmaker/handlers/mbs.py b/freshmaker/handlers/mbs.py index 85ecac0..61aeb8d 100644 --- a/freshmaker/handlers/mbs.py +++ b/freshmaker/handlers/mbs.py @@ -23,9 +23,9 @@ import requests -from freshmaker import log, conf +from freshmaker import log, conf, pdc, utils from freshmaker.handlers import BaseHandler -from freshmaker.events import ModuleBuilt, TestingEvent, ModuleMetadataUpdated +from freshmaker.events import ModuleBuilt, ModuleMetadataUpdated class MBS(BaseHandler): @@ -71,11 +71,49 @@ class MBS(BaseHandler): return [] def handle_module_built(self, event): - log.info("Triggering rebuild of modules depending on %r " - "in MBS" % event) + """ + When there is any module built and state is 'ready', query PDC to get + all modules that depends on this module, rebuild all these depending + modules. + """ + module_name = event.module_name + module_stream = event.module_stream + + log.info("Triggering rebuild of modules depending on %s:%s " + "in MBS", module_name, module_stream) + + pdc_session = pdc.get_client_session(conf) + depending_modules = pdc.get_modules(pdc_session, + build_dep_name=module_name, + build_dep_stream=module_stream, + active=True) + + # only rebuild the latest (by cmp variant_release) modules of + # (variant_name, variant_version) + latest_modules = [] + for (name, version) in set([(m.get('variant_name'), m.get('variant_version')) for m in depending_modules]): + mods = pdc.get_modules(pdc_session, name=name, version=version, active=True) + latest_modules.append(sorted(mods, key=lambda x: x['variant_release']).pop()) + + rebuild_modules = list(filter(lambda x: x in latest_modules, depending_modules)) + for mod in rebuild_modules: + module_name = mod['variant_name'] + module_stream = mod['variant_version'] + commitid = None + with utils.temp_dir(prefix='freshmaker-%s-' % module_name) as repodir: + try: + utils.clone_module_repo(module_name, repodir, branch=module_stream, user=conf.git_user, logger=log) + utils.add_empty_commit(repodir, msg="Bumped to rebuild because of %s update" % module_name, logger=log) + commitid = utils.get_commit_hash(repodir) + utils.push_repo(repodir) + except Exception: + log.exception("Failed to update module repo for '%s-%s'.", module_name, module_stream) + + if commitid is not None: + scm_url = conf.git_base_url + '/modules/%s.git?#%s' % (module_name, commitid) + self.rebuild_module(scm_url, module_stream) - # TODO: Just for initial testing of consumer - return [TestingEvent("ModuleBuilt handled")] + return [] def handle(self, event): if isinstance(event, ModuleMetadataUpdated): diff --git a/freshmaker/parsers/mbsmodule.py b/freshmaker/parsers/mbsmodule.py index d16a59f..e2c4259 100644 --- a/freshmaker/parsers/mbsmodule.py +++ b/freshmaker/parsers/mbsmodule.py @@ -50,5 +50,8 @@ class MBSModuleParser(BaseParser): 'topic "{0}"').format(topic)) return None - return ModuleBuilt(msg_id, msg_inner_msg.get('id'), - msg_inner_msg.get('state')) + return ModuleBuilt(msg_id, + msg_inner_msg.get('id'), + msg_inner_msg.get('state'), + msg_inner_msg.get('name'), + msg_inner_msg.get('stream')) diff --git a/freshmaker/pdc.py b/freshmaker/pdc.py new file mode 100644 index 0000000..b052273 --- /dev/null +++ b/freshmaker/pdc.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +import inspect +import requests +from pdc_client import PDCClient + +import freshmaker +import freshmaker.utils + + +def get_client_session(config): + """ + :param config: instance of freshmaker.config.Config + :return: pdc_client.PDCClient instance + """ + if 'ssl_verify' in inspect.getargspec(PDCClient.__init__).args: + # New API + return PDCClient( + server=config.pdc_url, + develop=config.pdc_develop, + ssl_verify=not config.pdc_insecure, + ) + else: + # Old API + return PDCClient( + server=config.pdc_url, + develop=config.pdc_develop, + insecure=config.pdc_insecure, + ) + + +@freshmaker.utils.retry(wait_on=(requests.ConnectTimeout, requests.ConnectionError), logger=freshmaker.log) +def get_modules(pdc_session, name=None, version=None, build_dep_name=None, build_dep_stream=None, active=True): + """ + :param pdc_session: PDCClient instance + :return: list of modules + """ + query = {} + if name is not None: + query['variant_name'] = name + if version is not None: + query['variant_version'] = version + if build_dep_name is not None: + query['build_dep_name'] = build_dep_name + if build_dep_stream is not None: + query['build_dep_stream'] = build_dep_stream + if active: + query['active'] = 'true' + + modules = pdc_session['unreleasedvariants'](page_size=-1, **query) + return modules diff --git a/freshmaker/utils.py b/freshmaker/utils.py new file mode 100644 index 0000000..c7c5622 --- /dev/null +++ b/freshmaker/utils.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +import contextlib +import errno +import functools +import getpass +import os +import shutil +import subprocess +import tempfile +import time + +from freshmaker import conf + + +def retry(timeout=conf.net_timeout, interval=conf.net_retry_interval, wait_on=Exception, logger=None): + """A decorator that allows to retry a section of code until success or timeout.""" + def wrapper(function): + @functools.wraps(function) + def inner(*args, **kwargs): + start = time.time() + while True: + if (time.time() - start) >= timeout: + raise # This re-raises the last exception. + try: + return function(*args, **kwargs) + except wait_on as e: + if logger is not None: + logger.warn("Exception %r raised from %r. Retry in %rs", + e, function, interval) + time.sleep(interval) + return inner + return wrapper + + +def makedirs(path, mode=0o775): + try: + os.makedirs(path, mode=mode) + except OSError as ex: + if ex.errno != errno.EEXIST: + raise + + +@contextlib.contextmanager +def temp_dir(logger=None, *args, **kwargs): + """Create a temporary directory and ensure it's deleted.""" + if kwargs.get('dir'): + # If we are supposed to create the temp dir in a particular location, + # ensure the location already exists. + makedirs(kwargs['dir']) + dir = tempfile.mkdtemp(*args, **kwargs) + try: + yield dir + finally: + try: + shutil.rmtree(dir) + except OSError as exc: + # Okay, we failed to delete temporary dir. + if logger: + logger.warn('Error removing %s: %s', dir, exc.strerror) + + +def clone_module_repo(name, dest, branch='master', user=None, logger=None): + """Clone a module repo""" + if user is None: + user = getpass.getuser() + cmd = ['git', 'clone', '-b', branch, os.path.join(conf.git_ssh_base_url % user, 'modules', name), dest] + _run_command(cmd, logger=logger) + + +def add_empty_commit(repo, msg="bump", author=None, logger=None): + """Commit an empty commit to repo""" + if author is None: + author = conf.git_author + cmd = ['git', 'commit', '--allow-empty', '-m', msg, '--author={}'.format(author)] + _run_command(cmd, logger=logger, rundir=repo) + + +def push_repo(repo, user=None, logger=None): + """Push repo""" + if user is None: + user = getpass.getuser() + cmd = ['git', 'push'] + _run_command(cmd, logger=logger, rundir=repo) + + +def get_commit_hash(repo, revision='HEAD'): + """Get commit hash from revision""" + cmd = ['git', 'rev-parse', revision] + return _run_command(cmd, rundir=repo, return_output=True).strip() + + +def _run_command(command, logger=None, rundir='/tmp', output=subprocess.PIPE, error=subprocess.PIPE, env=None, return_output=False): + """Run a command, return output if return_output is True. Error out if command exit with non-zero code.""" + + if logger: + logger.info("Running %s", subprocess.list2cmdline(command)) + + p1 = subprocess.Popen(command, cwd=rundir, stdout=output, stderr=error, universal_newlines=True, env=env, + close_fds=True) + (out, err) = p1.communicate() + + if out and logger: + logger.debug(out) + + if p1.returncode != 0: + if logger: + logger.error("Got an error from %s", command[0]) + logger.error(err) + raise OSError("Got an error (%d) from %s: %s" % (p1.returncode, command[0], err)) + if return_output: + return out diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..47a349b --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,137 @@ +# Copyright (c) 2017 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import random +import six +import string +import time +import uuid + + +BUILD_STATES = { + "init": 0, + "wait": 1, + "build": 2, + "done": 3, + "failed": 4, + "ready": 5, +} + + +class FedMsgFactory(object): + def __init__(self, *args, **kwargs): + self.msg_id = "%s-%s" % (time.strftime("%Y"), uuid.uuid4()) + self.msg = {} + self.signature = '123' + self.source_name = 'unittest', + self.source_version = '0.1.1', + self.timestamp = time.time() + self.topic = 'org.fedoraproject.prod.mbs.module.state.change' + self.username = 'freshmaker' + self.i = random.randint(0, 100) + self.inner_msg = {} + + def produce(self): + message_body = { + 'i': self.i, + 'msg_id': self.msg_id, + 'topic': self.topic, + 'username': self.username, + 'timestamp': self.timestamp, + 'signature': self.signature, + 'source_name': self.source_name, + 'source_version': self.source_version, + 'msg': self.inner_msg, + } + return { + 'body': message_body, + 'topic': self.topic + } + + +class ModuleBuiltMessage(FedMsgFactory): + def __init__(self, name, stream, state='ready', build_id=None, *args, **kwargs): + super(ModuleBuiltMessage, self).__init__(*args, **kwargs) + states_dict = {} + self.name = name + self.stream = stream + self.state = state + self.build_id = build_id if build_id else random.randint(0, 1000) + self.scmurl = "git://pkgs.fedoraproject.org/modules/%s?#%s" % (self.name, '123') + + for state, code in six.iteritems(BUILD_STATES): + states_dict[state] = {'state_name': state, 'state': code} + + inner_msg = { + 'component_builds': [], + 'id': self.build_id, + 'modulemd': '', + 'name': self.name, + 'owner': 'freshmaker', + 'scmurl': self.scmurl, + 'state': states_dict[self.state]['state'], + 'state_name': self.state, + 'state_reason': None, + 'state_trace': [], + 'state_url': u'/module-build-service/1/module-builds/%s' % self.build_id, + 'stream': u'master', + 'tasks': {}, + 'time_completed': None, + 'time_modified': None, + 'time_submitted': time.time(), + 'version': time.strftime("%Y%m%d%H%M%S"), + } + self.inner_msg = inner_msg + + +class PDCModuleInfoFactory(object): + def __init__(self, name, version, release, active=True): + self.variant_name = name + self.variant_version = version + self.variant_release = release + self.active = active + self.variant_uid = "%s-%s-%s" % (name, version, release) + self.variant_id = name + self.variant_type = 'module' + self.modulemd = '' + self.build_deps = [] + self.runtime_deps = [] + self.koji_tag = 'module-%s' % ''.join([random.choice(string.ascii_letters[:6] + string.digits) for n in range(16)]) + + def produce(self): + module = { + 'active': self.active, + 'variant_type': self.variant_type, + 'variant_id': self.variant_id, + 'variant_name': self.variant_name, + 'variant_version': self.variant_version, + 'variant_release': self.variant_release, + 'variant_uid': self.variant_uid, + 'modulemd': self.modulemd, + 'koji_tag': self.koji_tag, + 'build_deps': self.build_deps, + 'runtime_deps': self.runtime_deps, + } + return module + + +class PDCModuleInfo(PDCModuleInfoFactory): + def add_build_dep(self, name, stream): + self.build_deps.append({'dependency': name, 'stream': stream}) diff --git a/tests/test_consumer.py b/tests/test_consumer.py index 6be6912..353402c 100644 --- a/tests/test_consumer.py +++ b/tests/test_consumer.py @@ -32,8 +32,9 @@ class ConsumerTest(unittest.TestCase): def tearDown(self): pass + @mock.patch("freshmaker.handlers.mbs.MBS.handle_module_built") @mock.patch("freshmaker.consumer.get_global_consumer") - def test_consumer_processing_message(self, global_consumer): + def test_consumer_processing_message(self, global_consumer, handle_module_built): """ Tests that consumer parses the message, forwards the event to proper handler and is able to get the further work from @@ -41,6 +42,7 @@ class ConsumerTest(unittest.TestCase): """ hub = mock.MagicMock() hub.config = fedmsg.config.load_config() + hub.config['freshmakerconsumer'] = True consumer = freshmaker.consumer.FreshmakerConsumer(hub) global_consumer.return_value = consumer @@ -55,6 +57,7 @@ class ConsumerTest(unittest.TestCase): } }} + handle_module_built.return_value = [freshmaker.events.TestingEvent("ModuleBuilt handled")] consumer.consume(msg) event = consumer.incoming.get() diff --git a/tests/test_mbs_handler.py b/tests/test_mbs_handler.py new file mode 100644 index 0000000..d9dd52f --- /dev/null +++ b/tests/test_mbs_handler.py @@ -0,0 +1,158 @@ +# Copyright (c) 2017 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import sys +import unittest +import mock + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from tests import helpers + +from freshmaker import events +from freshmaker.handlers.mbs import MBS +from freshmaker.parsers.mbsmodule import MBSModuleParser + + +class MBSHandlerTest(unittest.TestCase): + def setUp(self): + events.BaseEvent.register_parser(MBSModuleParser) + + def _get_event(self, message): + event = events.BaseEvent.from_fedmsg(message['body']['topic'], message['body']) + return event + + def test_can_handle_module_built_ready_event(self): + """ + Tests MBS handler can handle modult build ready message + """ + + msg = helpers.ModuleBuiltMessage('testmodule', 'master', state='ready').produce() + event = self._get_event(msg) + + handler = MBS() + self.assertTrue(handler.can_handle(event)) + + def test_can_not_handle_module_built_non_ready_event(self): + """ + Tests MBS handler cannot handle modult build message which is not with + 'ready' state. + """ + for s in ['init', 'wait', 'build', 'done', 'failed']: + msg = helpers.ModuleBuiltMessage('testmodule', 'master', state=s).produce() + event = self._get_event(msg) + + handler = MBS() + self.assertFalse(handler.can_handle(event)) + + @mock.patch('freshmaker.handlers.mbs.utils') + @mock.patch('freshmaker.handlers.mbs.pdc') + @mock.patch('freshmaker.handlers.mbs.conf') + def test_rebuild_depending_modules_on_module_built_event(self, conf, pdc, utils): + """ + Tests MBS handler can rebuild all modules which depend on the module + in module built event. + """ + msg = helpers.ModuleBuiltMessage('testmodule', 'master', state='ready').produce() + event = self._get_event(msg) + + handler = MBS() + + mod2_r1_info = helpers.PDCModuleInfo('testmodule2', 'master', '20170412010101') + mod2_r1_info.add_build_dep('testmodule', 'master') + mod2_r1 = mod2_r1_info.produce() + + mod3_r1_info = helpers.PDCModuleInfo('testmodule3', 'master', '20170412010201') + mod3_r1_info.add_build_dep('testmodule', 'master') + mod3_r1 = mod3_r1_info.produce() + + def get_modules(pdc_session, name=None, version=None, build_dep_name=None, build_dep_stream=None, active=True): + + if name == 'testmodule2' and version == 'master': + return [mod2_r1] + elif name == 'testmodule3' and version == 'master': + return [mod3_r1] + else: + return [mod2_r1, mod3_r1] + + pdc.get_modules.side_effect = get_modules + conf.git_base_url = "git://pkgs.fedoraproject.org" + utils.get_commit_hash.side_effect = [ + "fae7848fa47a854f25b782aa64441040a6d86544", + "43ec03000d249231bc7135b11b810afc96e90efb", + ] + handler.rebuild_module = mock.Mock() + handler.handle_module_built(event) + + self.assertEqual(handler.rebuild_module.call_args_list, + [mock.call(u'git://pkgs.fedoraproject.org/modules/testmodule2.git?#fae7848fa47a854f25b782aa64441040a6d86544', u'master'), + mock.call(u'git://pkgs.fedoraproject.org/modules/testmodule3.git?#43ec03000d249231bc7135b11b810afc96e90efb', u'master')]) + + @mock.patch('freshmaker.handlers.mbs.utils') + @mock.patch('freshmaker.handlers.mbs.pdc') + @mock.patch('freshmaker.handlers.mbs.conf') + def test_only_rebuild_latest_depending_modules_on_module_built_event(self, conf, pdc, utils): + """ + Tests MBS handler only rebuild latest depending modules. If there is a + module only has old release depends on the module, it won't be rebuilt. + """ + msg = helpers.ModuleBuiltMessage('testmodule', 'master', state='ready').produce() + event = self._get_event(msg) + + handler = MBS() + + mod2_r1_info = helpers.PDCModuleInfo('testmodule2', 'master', '20170412010101') + mod2_r1_info.add_build_dep('testmodule', 'master') + mod2_r1 = mod2_r1_info.produce() + + mod3_r1_info = helpers.PDCModuleInfo('testmodule3', 'master', '20170412010101') + mod3_r1_info.add_build_dep('testmodule', 'master') + mod3_r1 = mod3_r1_info.produce() + + mod3_r2_info = helpers.PDCModuleInfo('testmodule3', 'master', '20170412010201') + mod3_r2_info.add_build_dep('testmodule', 'master') + mod3_r2 = mod3_r2_info.produce() + + def get_modules(pdc_session, name=None, version=None, build_dep_name=None, build_dep_stream=None, active=True): + + if name == 'testmodule2' and version == 'master': + return [mod2_r1] + elif name == 'testmodule3' and version == 'master': + return [mod3_r1, mod3_r2] + else: + return [mod2_r1, mod3_r1] + + # query for testmodule3 releases, get mod3_r1 and mod3_r2, + # only mod3_r1 depends on testmodule, and r1 < r2. + pdc.get_modules.side_effect = get_modules + conf.git_base_url = "git://pkgs.fedoraproject.org" + utils.get_commit_hash.side_effect = [ + "fae7848fa47a854f25b782aa64441040a6d86544", + "43ec03000d249231bc7135b11b810afc96e90efb", + ] + handler.rebuild_module = mock.Mock() + handler.handle_module_built(event) + + self.assertEqual(handler.rebuild_module.call_args_list, + [mock.call(u'git://pkgs.fedoraproject.org/modules/testmodule2.git?#fae7848fa47a854f25b782aa64441040a6d86544', u'master')]) + + +if __name__ == '__main__': + unittest.main()