From 10744268d010409cbefdd18ebd6cab089ace75ad Mon Sep 17 00:00:00 2001 From: Adam Williamson Date: Thu, 10 Jun 2021 14:27:28 -0700 Subject: [PATCH] Consumers: replace Greenwave with ResultsDB and WaiverDB (#4224) This replaces the consumer that listens for Greenwave 'decision change' messages with consumers that listen for ResultsDB and WaiverDB "new result" / "new waiver" messages and update gating decisions if the message may cause a change. The goal here is to make it possible to remove the "decision change" message code from Greenwave entirely, because in order to publish those messages, Greenwave has to duplicate a lot of Bodhi's knowledge about what kinds of decisions to ask for. This being in Greenwave is really against the intended design of the system, and means whenever anything changes about what kinds of decision Bodhi might request, we need to change both Bodhi and the Greenwave "decision change" publishing code. Signed-off-by: Adam Williamson --- bodhi/server/consumers/__init__.py | 8 +- bodhi/server/consumers/greenwave.py | 101 ------ bodhi/server/consumers/resultsdb.py | 78 +++++ bodhi/server/consumers/util.py | 68 ++++ bodhi/server/consumers/waiverdb.py | 67 ++++ .../tests/server/consumers/test_consumers.py | 17 +- .../tests/server/consumers/test_greenwave.py | 172 ---------- .../tests/server/consumers/test_resultsdb.py | 309 ++++++++++++++++++ bodhi/tests/server/consumers/test_waiverdb.py | 228 +++++++++++++ 9 files changed, 769 insertions(+), 279 deletions(-) delete mode 100644 bodhi/server/consumers/greenwave.py create mode 100644 bodhi/server/consumers/resultsdb.py create mode 100644 bodhi/server/consumers/util.py create mode 100644 bodhi/server/consumers/waiverdb.py delete mode 100644 bodhi/tests/server/consumers/test_greenwave.py create mode 100644 bodhi/tests/server/consumers/test_resultsdb.py create mode 100644 bodhi/tests/server/consumers/test_waiverdb.py diff --git a/bodhi/server/consumers/__init__.py b/bodhi/server/consumers/__init__.py index f2e625f38a..6e53e95f07 100644 --- a/bodhi/server/consumers/__init__.py +++ b/bodhi/server/consumers/__init__.py @@ -30,8 +30,9 @@ from bodhi.server.config import config from bodhi.server.consumers.automatic_updates import AutomaticUpdateHandler from bodhi.server.consumers.signed import SignedHandler -from bodhi.server.consumers.greenwave import GreenwaveHandler from bodhi.server.consumers.ci import CIHandler +from bodhi.server.consumers.resultsdb import ResultsdbHandler +from bodhi.server.consumers.waiverdb import WaiverdbHandler log = logging.getLogger('bodhi') @@ -53,8 +54,9 @@ def __init__(self): self.handler_infos = [ HandlerInfo('.buildsys.tag', "Signed", SignedHandler()), HandlerInfo('.buildsys.tag', 'Automatic Update', AutomaticUpdateHandler()), - HandlerInfo('.greenwave.decision.update', 'Greenwave', GreenwaveHandler()), - HandlerInfo('.ci.koji-build.test.running', 'CI', CIHandler()) + HandlerInfo('.ci.koji-build.test.running', 'CI', CIHandler()), + HandlerInfo('.waiverdb.waiver.new', 'WaiverDB', WaiverdbHandler()), + HandlerInfo('.resultsdb.result.new', 'ResultsDB', ResultsdbHandler()), ] def __call__(self, msg: fedora_messaging.api.Message): # noqa: D401 diff --git a/bodhi/server/consumers/greenwave.py b/bodhi/server/consumers/greenwave.py deleted file mode 100644 index f7ea3eae8c..0000000000 --- a/bodhi/server/consumers/greenwave.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright © 2019 Red Hat, Inc. -# -# This file is part of Bodhi. -# -# 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; either version 2 -# of the License, or (at your option) any later version. -# -# 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Software Foundation, Inc., 51 -# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -""" -The "greenwave handler". - -This module is responsible for listening for messages from greenwave. -It then updates the policies of the build that greenwave checked. -""" - -import logging - -import fedora_messaging - -from bodhi.server.models import Build, TestGatingStatus -from bodhi.server.util import transactional_session_maker - -log = logging.getLogger(__name__) - - -class GreenwaveHandler: - """ - The Bodhi Greenwave Handler. - - A fedora-messaging listener waiting for messages from greenwave about enforced policies. - """ - - def __init__(self): - """Initialize the GreenwaveHandler.""" - self.db_factory = transactional_session_maker() - - def __call__(self, message: fedora_messaging.api.Message): - """Handle messages arriving with the configured topic.""" - msg = message.body - if not msg: - log.debug("Ignoring message without body.") - return - - subject_identifier = msg.get("subject_identifier") - - if subject_identifier is None: - log.debug("Couldn't find subject_identifier in Greenwave message") - return - - subject_type = msg.get("subject_type") - if subject_type == "compose": - log.debug("Not requesting a decision for a compose") - return - - subject_identifier = msg.get("subject_identifier") - - if "policies_satisfied" not in msg: - log.debug("Couldn't find policies_satisfied in Greenwave message") - return - - with self.db_factory(): - - build = Build.get(subject_identifier) - if build is None: - log.debug(f"Couldn't find build {subject_identifier} in DB") - return - - update = build.update - log.info(f"Updating the test_gating_status for: {update.alias}") - if len(update.builds) > 1: - update.update_test_gating_status() - else: - update.test_gating_status = self._extract_gating_status(msg) - - def _extract_gating_status(self, msg): - """ - Extract gating information from the Greenwave message and return it. - - Returns: - TestGatingStatus: - - TestGatingStatus.ignored if no tests are required - - TestGatingStatus.failed if policies are not satisfied - - TestGatingStatus.passed if policies are satisfied, and there - are required tests - """ - if not msg['policies_satisfied']: - return TestGatingStatus.failed - if msg['summary'] == 'no tests are required': - # If an unrestricted policy is applied and no tests are required - # on this update, let's set the test gating as ignored in Bodhi. - return TestGatingStatus.ignored - return TestGatingStatus.passed diff --git a/bodhi/server/consumers/resultsdb.py b/bodhi/server/consumers/resultsdb.py new file mode 100644 index 0000000000..bfba5d7fcb --- /dev/null +++ b/bodhi/server/consumers/resultsdb.py @@ -0,0 +1,78 @@ +# Copyright Red Hat and others. +# +# This file is part of Bodhi. +# +# 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; either version 2 +# of the License, or (at your option) any later version. +# +# 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +The "resultsdb handler". + +This module is responsible for listening for messages from ResultsDB. +If a message seems like it might be a result change for an update or +build from an update, we re-check the gating decision for that update. +""" + +import logging + +import fedora_messaging + +from bodhi.server.consumers.util import update_from_db_message +from bodhi.server.models import TestGatingStatus +from bodhi.server.util import transactional_session_maker + +log = logging.getLogger(__name__) + + +class ResultsdbHandler: + """ + The Bodhi ResultsDB Handler. + + A fedora-messaging listener waiting for messages from resultsdb that may + affect gating status for an update. + """ + + def __init__(self): + """Initialize the handler.""" + self.db_factory = transactional_session_maker() + + def __call__(self, message: fedora_messaging.api.Message): + """Handle messages arriving with the configured topic.""" + msg = message.body + if not msg: + log.debug("Ignoring message without body.") + return + + passed = msg.get("outcome") in ("PASSED", "INFO") + + data = msg.get("data") + if not data: + log.error(f"Couldn't find data dict in ResultsDB message {message.id}") + return + + with self.db_factory(): + # find the update + update = update_from_db_message(message.id, msg["data"]) + if not update: + # update_from_db_message will already have logged why + return + # update the gating status if there's a chance it changed + status = update.test_gating_status + if ( + (passed and status == TestGatingStatus.passed) + or (not passed and status == TestGatingStatus.failed) + ): + log.debug("Not updating test_gating_status as no chance of a change") + return + log.info(f"Updating the test_gating_status for: {update.alias}") + update.update_test_gating_status() diff --git a/bodhi/server/consumers/util.py b/bodhi/server/consumers/util.py new file mode 100644 index 0000000000..03edc51e76 --- /dev/null +++ b/bodhi/server/consumers/util.py @@ -0,0 +1,68 @@ +# Copyright Red Hat and others. +# +# This file is part of Bodhi. +# +# 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; either version 2 +# of the License, or (at your option) any later version. +# +# 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +"""Utility functions for message consumers.""" + +import logging + +from bodhi.server.models import Build, Update + +log = logging.getLogger(__name__) + +def update_from_db_message(msgid: str, itemdict: dict): + """Find and return the relevant Bodhi update for a waiverdb or + resultsdb fedora_messaging message. Used by the resultsdb and + waiverdb consumers. + + Args: + msgid: the message ID (for logging purposes) + itemdict: the relevant dict from the message. 'subject' dict + for a waiverdb message, 'item' dict for resultsdb. + Returns: + bodhi.server.models.Update or None: the relevant update, if + found. + """ + itemtype = itemdict.get("type") + if not itemtype: + log.error(f"Couldn't find item type in message {msgid}") + return None + if itemtype not in ("koji_build", "bodhi_update"): + log.debug(f"Irrelevant item type {itemtype}") + return None + + # find the update + if itemtype == "bodhi_update": + updateid = itemdict.get("item") + if not updateid: + log.error(f"Couldn't find update ID in message {msgid}") + return None + update = Update.get(updateid) + if not update: + log.error(f"Couldn't find update {updateid} in DB") + return None + else: + nvr = itemdict.get("nvr", itemdict.get("item")) + if not nvr: + log.error(f"Couldn't find nvr in message {msgid}") + return None + build = Build.get(nvr) + if not build: + log.error(f"Couldn't find build {nvr} in DB") + return None + update = build.update + + return update diff --git a/bodhi/server/consumers/waiverdb.py b/bodhi/server/consumers/waiverdb.py new file mode 100644 index 0000000000..c27f1a4950 --- /dev/null +++ b/bodhi/server/consumers/waiverdb.py @@ -0,0 +1,67 @@ +# Copyright Red Hat and others. +# +# This file is part of Bodhi. +# +# 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; either version 2 +# of the License, or (at your option) any later version. +# +# 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +The "waiverdb handler". + +This module is responsible for listening for 'new waiver' messages from +WaiverDB, and re-checking the gating decision for the relevant update. +""" + +import logging + +import fedora_messaging + +from bodhi.server.consumers.util import update_from_db_message +from bodhi.server.models import TestGatingStatus +from bodhi.server.util import transactional_session_maker + +log = logging.getLogger(__name__) + + +class WaiverdbHandler: + """ + The Bodhi WaiverDB Handler. + + A fedora-messaging listener waiting for messages from WaiverDB and + updating gating status of the relevant update. + """ + + def __init__(self): + """Initialize the handler.""" + self.db_factory = transactional_session_maker() + + def __call__(self, message: fedora_messaging.api.Message): + """Handle messages arriving with the configured topic.""" + msg = message.body + if not msg: + log.debug("Ignoring message without body.") + return + + subject = msg.get("subject") + if subject is None: + log.error(f"Couldn't find subject in WaiverDB message {message.id}") + return + + with self.db_factory(): + # find the update + update = update_from_db_message(message.id, subject) + # update the gating status unless it's already "passed", a + # waiver can't change it from passed to anything else + if update and update.test_gating_status != TestGatingStatus.passed: + log.info(f"Updating the test_gating_status for: {update.alias}") + update.update_test_gating_status() diff --git a/bodhi/tests/server/consumers/test_consumers.py b/bodhi/tests/server/consumers/test_consumers.py index 95569523b8..bae0f77517 100644 --- a/bodhi/tests/server/consumers/test_consumers.py +++ b/bodhi/tests/server/consumers/test_consumers.py @@ -97,10 +97,21 @@ def test_messaging_callback_signed_automatic_update(self, signed_handler.assert_called_once_with(msg) automatic_update_handler.assert_called_once_with(msg) - @mock.patch('bodhi.server.consumers.GreenwaveHandler') - def test_messaging_callback_greenwave(self, Handler): + @mock.patch('bodhi.server.consumers.ResultsdbHandler') + def test_messaging_callback_resultsdb(self, Handler): msg = Message( - topic="org.fedoraproject.prod.greenwave.decision.update", + topic="org.fedoraproject.prod.resultsdb.result.new", + body={} + ) + handler = mock.Mock() + Handler.side_effect = lambda: handler + Consumer()(msg) + handler.assert_called_once_with(msg) + + @mock.patch('bodhi.server.consumers.WaiverdbHandler') + def test_messaging_callback_waiverdb(self, Handler): + msg = Message( + topic="org.fedoraproject.prod.waiverdb.waiver.new", body={} ) handler = mock.Mock() diff --git a/bodhi/tests/server/consumers/test_greenwave.py b/bodhi/tests/server/consumers/test_greenwave.py deleted file mode 100644 index b7b80ff4cb..0000000000 --- a/bodhi/tests/server/consumers/test_greenwave.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright © 2019 Red Hat, Inc. and others. -# -# This file is part of Bodhi. -# -# 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; either version 2 -# of the License, or (at your option) any later version. -# -# 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -"""This test suite contains tests for the bodhi.server.consumers.greenwave module.""" - -from unittest import mock - -from fedora_messaging.api import Message - -from bodhi.server import models -from bodhi.server.consumers import greenwave -from bodhi.server.config import config -from bodhi.tests.server import create_update -from bodhi.tests.server.base import BasePyTestCase, TransactionalSessionMaker - - -class TestGreenwaveHandler(BasePyTestCase): - """Test class for the :func:`GreenwaveHandler` method.""" - - def setup_method(self, method): - super(TestGreenwaveHandler, self).setup_method(method) - self.sample_message = Message( - topic="org.fedoraproject.prod.greenwave.decision.update", - body={ - "subject_identifier": "bodhi-2.0-1.fc17", - "subject_type": "koji_build", - 'policies_satisfied': True, - 'summary': "all tests have passed", - }, - ) - self.handler = greenwave.GreenwaveHandler() - self.handler.db_factory = TransactionalSessionMaker(self.Session) - self.single_build_update = self.db.query(models.Update).filter( - models.Build.nvr == 'bodhi-2.0-1.fc17').one() - - def test_single_build(self): - """ - Assert that a greenwave message updates the gating status of an update. - """ - update = self.single_build_update - - # before the greenwave consumer run the gating tests status is None - assert update.test_gating_status is None - - with mock.patch('bodhi.server.models.util.greenwave_api_post') as mock_greenwave: - self.handler(self.sample_message) - # Only one build, the info should come from the message and not the greenwave API - assert mock_greenwave.called is False - - # After the consumer run the gating tests status was updated. - assert update.test_gating_status == models.TestGatingStatus.passed - - def test_single_build_failed(self): - """ - Assert that a greenwave message updates the gating status of an update when gating status is - failed. - """ - update = self.single_build_update - - self.sample_message.body["policies_satisfied"] = False - self.handler(self.sample_message) - assert update.test_gating_status == models.TestGatingStatus.failed - - def test_single_build_ignored(self): - """ - Assert that a greenwave message updates the gating status of an update when gating status is - ignored. - """ - update = self.single_build_update - - self.sample_message.body["policies_satisfied"] = True - self.sample_message.body["summary"] = "no tests are required" - self.handler(self.sample_message) - assert update.test_gating_status == models.TestGatingStatus.ignored - - @mock.patch.dict(config, [('greenwave_api_url', 'http://domain.local')]) - def test_multiple_builds(self): - """ - Assert that a greenwave message updates the gating tests status of an update. - """ - # Create an update with multiple builds - with mock.patch(target='uuid.uuid4', return_value='multiplebuilds'): - update = create_update( - self.db, ['MultipleBuild1-1.0-1.fc17', 'MultipleBuild2-1.0-1.fc17'] - ) - update.type = models.UpdateType.bugfix - update.severity = models.UpdateSeverity.medium - self.db.flush() - - # Reference it in the incoming message - self.sample_message.body["subject_identifier"] = "MultipleBuild1-1.0-1.fc17" - - # Put bogus info in the message to make sure it does not get used - self.sample_message.body["policies_satisfied"] = False - self.sample_message.body["summary"] = "this should not be used" - - # before the greenwave consumer run the gating tests status is None - assert update.test_gating_status is None - - with mock.patch('bodhi.server.models.util.greenwave_api_post') as mock_greenwave: - greenwave_response = { - 'policies_satisfied': True, - 'summary': "all tests have passed" - } - mock_greenwave.return_value = greenwave_response - self.handler(self.sample_message) - - # After the consumer run the gating tests status was updated. - assert update.test_gating_status == models.TestGatingStatus.passed - - @mock.patch('bodhi.server.consumers.greenwave.log') - def test_greenwave_bad_message(self, mock_log): - """ Assert that the consumer ignores messages badly formed """ - bad_message = Message(topic="", body={}) - self.handler(bad_message) - assert mock_log.debug.call_count == 1 - mock_log.debug.assert_called_with("Ignoring message without body.") - - @mock.patch('bodhi.server.consumers.greenwave.log') - def test_greenwave_message_missing_subject_identifier(self, mock_log): - """ - Assert that the consumer raise an exception if we could not find the - subject_identifier in the message - """ - bad_message = Message(topic="", body={"foo": "bar"}) - self.handler(bad_message) - assert mock_log.debug.call_count == 1 - mock_log.debug.assert_called_with("Couldn't find subject_identifier in Greenwave message") - - @mock.patch('bodhi.server.consumers.greenwave.log') - def test_greenwave_message_missing_policies_satisfied(self, mock_log): - """ - Assert that the consumer raise an exception if we could not find the - policies_satisfied in the message - """ - bad_message = Message(topic="", body={"subject_identifier": "foobar"}) - self.handler(bad_message) - assert mock_log.debug.call_count == 1 - mock_log.debug.assert_called_with("Couldn't find policies_satisfied in Greenwave message") - - @mock.patch('bodhi.server.consumers.greenwave.log') - def test_greenwave_wrong_build_nvr(self, mock_log): - """ - Assert that the consumer raise an exception if we could not find the - subject_identifier (build nvr) in the DB. - """ - self.sample_message.body["subject_identifier"] = "notapackage-2.0-1.fc17" - self.handler(self.sample_message) - assert mock_log.debug.call_count == 1 - mock_log.debug.assert_called_with("Couldn't find build notapackage-2.0-1.fc17 in DB") - - @mock.patch('bodhi.server.consumers.greenwave.log') - def test_greenwave_compose_subject_type(self, mock_log): - """ Assert that the consumer ignores messages with subject_type equal to compose """ - self.sample_message.body["subject_type"] = "compose" - self.handler(self.sample_message) - assert mock_log.debug.call_count == 1 - mock_log.debug.assert_called_with("Not requesting a decision for a compose") diff --git a/bodhi/tests/server/consumers/test_resultsdb.py b/bodhi/tests/server/consumers/test_resultsdb.py new file mode 100644 index 0000000000..f356ee6041 --- /dev/null +++ b/bodhi/tests/server/consumers/test_resultsdb.py @@ -0,0 +1,309 @@ +# Copyright © 2019 Red Hat, Inc. and others. +# +# This file is part of Bodhi. +# +# 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; either version 2 +# of the License, or (at your option) any later version. +# +# 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +"""This test suite contains tests for the bodhi.server.consumers.resultsdb module.""" + +from unittest import mock + +from fedora_messaging.api import Message + +from bodhi.server import models +from bodhi.server.consumers import resultsdb +from bodhi.tests.server.base import BasePyTestCase, TransactionalSessionMaker + + +class TestResultsdbHandler(BasePyTestCase): + """Test class for the :func:`ResultsdbHandler` method.""" + + def setup_method(self, method): + super(TestResultsdbHandler, self).setup_method(method) + self.handler = resultsdb.ResultsdbHandler() + self.handler.db_factory = TransactionalSessionMaker(self.Session) + self.single_build_update = self.db.query(models.Update).filter( + models.Build.nvr == 'bodhi-2.0-1.fc17').one() + + def get_sample_message(self, typ="bodhi_update", passed=True): + """ + Returns a sample message, for the specified type and success + status. + """ + outcome = "PASSED" + if not passed: + outcome = "FAILED" + if typ == "bodhi_update": + data = {"item": self.single_build_update.alias, "type": "bodhi_update"} + elif typ == "koji_build": + nvr = self.single_build_update.builds[0].nvr + data = {"nvr": nvr, "item": nvr, "type": "koji_build"} + return Message( + topic="org.fedoraproject.prod.resultsdb.result.new", + body={ + "outcome": outcome, + "data": data + } + ) + + def test_resultsdb_passed_koji_test(self): + """ + Assert that a passed test ResultsDB message for a Koji build + from an update updates the gating status of the update if in + failed or waiting status, or with no status. + """ + update = self.single_build_update + + # before the greenwave consumer run the gating tests status is None + assert update.test_gating_status is None + + with mock.patch('bodhi.server.models.util.greenwave_api_post') as mock_greenwave: + greenwave_response = { + 'policies_satisfied': True, + 'summary': "All required tests passed", + 'applicable_policies': [ + 'kojibuild_bodhipush_no_requirements', + 'kojibuild_bodhipush_remoterule', + 'bodhiupdate_bodhipush_no_requirements', + 'bodhiupdate_bodhipush_openqa' + ], + 'satisfied_requirements': [ + { + 'result_id': 39603316, + 'subject_type': 'bodhi_update', + 'testcase': 'update.install_default_update_netinst', + 'type': 'test-result-passed' + }, + ], + 'unsatisfied_requirements': [] + } + mock_greenwave.return_value = greenwave_response + testmsg = self.get_sample_message(typ="koji_build", passed=True) + self.handler(testmsg) + assert update.test_gating_status == models.TestGatingStatus.passed + # now check failed + update.test_gating_status = models.TestGatingStatus.failed + self.handler(testmsg) + assert update.test_gating_status == models.TestGatingStatus.passed + # and waiting + update.test_gating_status = models.TestGatingStatus.waiting + self.handler(testmsg) + assert update.test_gating_status == models.TestGatingStatus.passed + # now check we don't update if already passed + with mock.patch("bodhi.server.models.Update.update_test_gating_status") as updmock: + self.handler(testmsg) + assert updmock.call_count == 0 + + def test_resultsdb_failed_koji_test(self): + """ + Assert that a failed test ResultsDB message for a Koji build + from an update updates the gating status of the update if in + passed or waiting status, or with no status. + """ + update = self.single_build_update + + # before the consumer run the gating tests status is None + assert update.test_gating_status is None + + with mock.patch('bodhi.server.models.util.greenwave_api_post') as mock_greenwave: + greenwave_response = { + 'policies_satisfied': False, + 'summary': "1 of 1 required tests failed", + 'applicable_policies': [ + 'kojibuild_bodhipush_no_requirements', + 'kojibuild_bodhipush_remoterule', + 'bodhiupdate_bodhipush_no_requirements', + 'bodhiupdate_bodhipush_openqa' + ], + 'satisfied_requirements': [], + 'unsatisfied_requirements': [ + { + 'item': { + 'type': 'bodhi_update' + }, + 'scenario': 'fedora.updates-everything-boot-iso.x86_64.64bit', + 'subject_type': 'bodhi_update', + 'testcase': 'update.install_default_update_netinst', + 'type': 'test-result-failed' + } + ] + } + mock_greenwave.return_value = greenwave_response + testmsg = self.get_sample_message(typ="koji_build", passed=False) + self.handler(testmsg) + assert update.test_gating_status == models.TestGatingStatus.failed + # now check failed + update.test_gating_status = models.TestGatingStatus.failed + self.handler(testmsg) + assert update.test_gating_status == models.TestGatingStatus.failed + # and waiting + update.test_gating_status = models.TestGatingStatus.waiting + self.handler(testmsg) + assert update.test_gating_status == models.TestGatingStatus.failed + # now check we don't update if already failed + with mock.patch("bodhi.server.models.Update.update_test_gating_status") as updmock: + self.handler(testmsg) + assert updmock.call_count == 0 + + def test_resultsdb_bodhi_tests(self): + """ + Assert that ResultsDB messages for tests on an update result in + the gating status of that update being updated. + """ + update = self.single_build_update + + with mock.patch('bodhi.server.models.util.greenwave_api_post') as mock_greenwave: + greenwave_response = { + 'policies_satisfied': True, + 'summary': "All required tests passed", + 'applicable_policies': [ + 'kojibuild_bodhipush_no_requirements', + 'kojibuild_bodhipush_remoterule', + 'bodhiupdate_bodhipush_no_requirements', + 'bodhiupdate_bodhipush_openqa' + ], + 'satisfied_requirements': [ + { + 'result_id': 39603316, + 'subject_type': 'bodhi_update', + 'testcase': 'update.install_default_update_netinst', + 'type': 'test-result-passed' + }, + ], + 'unsatisfied_requirements': [] + } + mock_greenwave.return_value = greenwave_response + # check the 'success' case + testmsg = self.get_sample_message(typ="bodhi_update", passed=True) + self.handler(testmsg) + assert update.test_gating_status == models.TestGatingStatus.passed + # now check the 'failure' case + testmsg = self.get_sample_message(typ="bodhi_update", passed=False) + greenwave_response = { + 'policies_satisfied': False, + 'summary': "1 of 1 required tests failed", + 'applicable_policies': [ + 'kojibuild_bodhipush_no_requirements', + 'kojibuild_bodhipush_remoterule', + 'bodhiupdate_bodhipush_no_requirements', + 'bodhiupdate_bodhipush_openqa' + ], + 'satisfied_requirements': [], + 'unsatisfied_requirements': [ + { + 'item': { + 'type': 'bodhi_update' + }, + 'scenario': 'fedora.updates-everything-boot-iso.x86_64.64bit', + 'subject_type': 'bodhi_update', + 'testcase': 'update.install_default_update_netinst', + 'type': 'test-result-failed' + } + ] + } + mock_greenwave.return_value = greenwave_response + self.handler(testmsg) + assert update.test_gating_status == models.TestGatingStatus.failed + + @mock.patch('bodhi.server.consumers.resultsdb.log') + def test_resultsdb_bad_message(self, mock_log): + """ Assert that the consumer ignores badly formed messages.""" + bad_message = Message(topic="", body={}) + self.handler(bad_message) + assert mock_log.debug.call_count == 1 + mock_log.debug.assert_called_with("Ignoring message without body.") + + @mock.patch('bodhi.server.consumers.resultsdb.log') + def test_resultsdb_message_missing_data(self, mock_log): + """ + Assert that the consumer logs and returns if we could not find the + data dict in the message. + """ + bad_message = Message(topic="", body={"foo": "bar"}) + self.handler(bad_message) + assert mock_log.error.call_count == 1 + mock_log.error.assert_called_with( + f"Couldn't find data dict in ResultsDB message {bad_message.id}") + + @mock.patch('bodhi.server.consumers.util.log') + def test_resultsdb_message_missing_result_type(self, mock_log): + """ + Assert that the consumer logs and returns if we could not find the + result type in the message. + """ + bad_message = Message(topic="", body={"data": {"foo": "bar"}}) + self.handler(bad_message) + assert mock_log.error.call_count == 1 + mock_log.error.assert_called_with( + f"Couldn't find item type in message {bad_message.id}") + + @mock.patch('bodhi.server.consumers.util.log') + def test_resultsdb_message_irrelevant_result_type(self, mock_log): + """ + Assert that the consumer logs and returns if the result type + is not a relevant one. + """ + bad_message = Message(topic="", body={"data": {"type": "foo"}}) + self.handler(bad_message) + assert mock_log.debug.call_count == 1 + mock_log.debug.assert_called_with("Irrelevant item type foo") + + @mock.patch('bodhi.server.consumers.util.log') + def test_resultsdb_koji_message_no_nvr(self, mock_log): + """ + Assert that the consumer logs and returns if a Koji result + message is missing the NVR. + """ + testmsg = self.get_sample_message(typ="koji_build", passed=True) + del testmsg.body["data"]["nvr"] + del testmsg.body["data"]["item"] + self.handler(testmsg) + assert mock_log.error.call_count == 1 + mock_log.error.assert_called_with(f"Couldn't find nvr in message {testmsg.id}") + + @mock.patch('bodhi.server.consumers.util.log') + def test_resultsdb_koji_message_wrong_build_nvr(self, mock_log): + """ + Assert that the consumer raise an exception if we could not find the + build nvr in the DB. + """ + testmsg = self.get_sample_message(typ="koji_build", passed=True) + testmsg.body["data"]["nvr"] = "notapackage-2.0-1.fc17" + self.handler(testmsg) + assert mock_log.error.call_count == 1 + mock_log.error.assert_called_with("Couldn't find build notapackage-2.0-1.fc17 in DB") + + @mock.patch('bodhi.server.consumers.util.log') + def test_resultsdb_bodhi_message_no_updateid(self, mock_log): + """ + Assert that the consumer logs and returns if a Bodhi result + message is missing the update ID. + """ + testmsg = self.get_sample_message(typ="bodhi_update", passed=True) + del testmsg.body["data"]["item"] + self.handler(testmsg) + assert mock_log.error.call_count == 1 + mock_log.error.assert_called_with(f"Couldn't find update ID in message {testmsg.id}") + + @mock.patch('bodhi.server.consumers.util.log') + def test_resultsdb_bodhi_message_wrong_updateid(self, mock_log): + """ + Assert that the consumer raise an exception if we could not find the + update ID in the DB. + """ + testmsg = self.get_sample_message(typ="bodhi_update", passed=True) + testmsg.body["data"]["item"] = "NOTANUPDATE" + self.handler(testmsg) + assert mock_log.error.call_count == 1 + mock_log.error.assert_called_with("Couldn't find update NOTANUPDATE in DB") diff --git a/bodhi/tests/server/consumers/test_waiverdb.py b/bodhi/tests/server/consumers/test_waiverdb.py new file mode 100644 index 0000000000..0eadb36875 --- /dev/null +++ b/bodhi/tests/server/consumers/test_waiverdb.py @@ -0,0 +1,228 @@ +# Copyright © 2019 Red Hat, Inc. and others. +# +# This file is part of Bodhi. +# +# 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; either version 2 +# of the License, or (at your option) any later version. +# +# 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +"""This test suite contains tests for the bodhi.server.consumers.waiverdb module.""" + +from unittest import mock + +from fedora_messaging.api import Message + +from bodhi.server import models +from bodhi.server.consumers import waiverdb +from bodhi.tests.server.base import BasePyTestCase, TransactionalSessionMaker + + +class TestWaiverdbHandler(BasePyTestCase): + """Test class for the :func:`WaiverdbHandler` method.""" + + def setup_method(self, method): + super(TestWaiverdbHandler, self).setup_method(method) + self.handler = waiverdb.WaiverdbHandler() + self.handler.db_factory = TransactionalSessionMaker(self.Session) + self.single_build_update = self.db.query(models.Update).filter( + models.Build.nvr == 'bodhi-2.0-1.fc17').one() + + def get_sample_message(self, typ="bodhi_update"): + """Returns a sample message for the specified type.""" + if typ == "bodhi_update": + item = self.single_build_update.alias + elif typ == "koji_build": + item = self.single_build_update.builds[0].nvr + return Message( + topic="org.fedoraproject.prod.waiverdb.waiver.new", + body={ + "subject_type": typ, + "subject": { + "item": item, + "type": typ + } + } + ) + + def test_waiverdb_koji_waiver(self): + """ + Assert that a Koji build waiver message updates the gating + status of the update, unless it's in passed status. + """ + update = self.single_build_update + + # before the greenwave consumer run the gating tests status is None + assert update.test_gating_status is None + + with mock.patch('bodhi.server.models.util.greenwave_api_post') as mock_greenwave: + greenwave_response = { + 'policies_satisfied': True, + 'summary': "All required tests passed", + 'applicable_policies': [ + 'kojibuild_bodhipush_no_requirements', + 'kojibuild_bodhipush_remoterule', + 'bodhiupdate_bodhipush_no_requirements', + 'bodhiupdate_bodhipush_openqa' + ], + 'satisfied_requirements': [ + { + 'result_id': 39603316, + 'subject_type': 'bodhi_update', + 'testcase': 'update.install_default_update_netinst', + 'type': 'test-result-passed' + }, + ], + 'unsatisfied_requirements': [] + } + mock_greenwave.return_value = greenwave_response + testmsg = self.get_sample_message(typ="koji_build") + self.handler(testmsg) + assert update.test_gating_status == models.TestGatingStatus.passed + # now check failed + update.test_gating_status = models.TestGatingStatus.failed + self.handler(testmsg) + assert update.test_gating_status == models.TestGatingStatus.passed + # and waiting + update.test_gating_status = models.TestGatingStatus.waiting + self.handler(testmsg) + assert update.test_gating_status == models.TestGatingStatus.passed + # now check we don't update if already passed + with mock.patch("bodhi.server.models.Update.update_test_gating_status") as updmock: + self.handler(testmsg) + assert updmock.call_count == 0 + + def test_waiverdb_bodhi_waiver(self): + """ + Assert that a Bodhi update waiver message updates the gating + status of the update. + """ + update = self.single_build_update + + # before the greenwave consumer run the gating tests status is None + assert update.test_gating_status is None + + with mock.patch('bodhi.server.models.util.greenwave_api_post') as mock_greenwave: + greenwave_response = { + 'policies_satisfied': True, + 'summary': "All required tests passed", + 'applicable_policies': [ + 'kojibuild_bodhipush_no_requirements', + 'kojibuild_bodhipush_remoterule', + 'bodhiupdate_bodhipush_no_requirements', + 'bodhiupdate_bodhipush_openqa' + ], + 'satisfied_requirements': [ + { + 'result_id': 39603316, + 'subject_type': 'bodhi_update', + 'testcase': 'update.install_default_update_netinst', + 'type': 'test-result-passed' + }, + ], + 'unsatisfied_requirements': [] + } + mock_greenwave.return_value = greenwave_response + testmsg = self.get_sample_message(typ="bodhi_update") + self.handler(testmsg) + assert update.test_gating_status == models.TestGatingStatus.passed + # don't bother testing every other path here too + + @mock.patch('bodhi.server.consumers.waiverdb.log') + def test_waiverdb_bad_message(self, mock_log): + """ Assert that the consumer ignores badly formed messages.""" + bad_message = Message(topic="", body={}) + self.handler(bad_message) + assert mock_log.debug.call_count == 1 + mock_log.debug.assert_called_with("Ignoring message without body.") + + @mock.patch('bodhi.server.consumers.waiverdb.log') + def test_waiverdb_message_missing_subject(self, mock_log): + """ + Assert that the consumer logs and returns if we could not find the + subject in the message. + """ + bad_message = Message(topic="", body={"foo": "bar"}) + self.handler(bad_message) + assert mock_log.error.call_count == 1 + mock_log.error.assert_called_with( + f"Couldn't find subject in WaiverDB message {bad_message.id}") + + @mock.patch('bodhi.server.consumers.util.log') + def test_waiverdb_message_missing_subject_type(self, mock_log): + """ + Assert that the consumer logs and returns if we could not find the + subject type in the message. + """ + bad_message = Message(topic="", body={"subject": {"foo": "bar"}}) + self.handler(bad_message) + assert mock_log.error.call_count == 1 + mock_log.error.assert_called_with( + f"Couldn't find item type in message {bad_message.id}") + + @mock.patch('bodhi.server.consumers.util.log') + def test_waiverdb_message_irrelevant_result_type(self, mock_log): + """ + Assert that the consumer logs and returns if the result type + is not a relevant one. + """ + bad_message = Message(topic="", body={"subject": {"type": "foo"}}) + self.handler(bad_message) + assert mock_log.debug.call_count == 1 + mock_log.debug.assert_called_with("Irrelevant item type foo") + + @mock.patch('bodhi.server.consumers.util.log') + def test_waiverdb_koji_message_no_nvr(self, mock_log): + """ + Assert that the consumer logs and returns if a Koji waiver + message is missing the NVR. + """ + testmsg = self.get_sample_message(typ="koji_build") + del testmsg.body["subject"]["item"] + self.handler(testmsg) + assert mock_log.error.call_count == 1 + mock_log.error.assert_called_with(f"Couldn't find nvr in message {testmsg.id}") + + @mock.patch('bodhi.server.consumers.util.log') + def test_waiverdb_koji_message_wrong_build_nvr(self, mock_log): + """ + Assert that the consumer raise an exception if we could not find the + build nvr in the DB. + """ + testmsg = self.get_sample_message(typ="koji_build") + testmsg.body["subject"]["item"] = "notapackage-2.0-1.fc17" + self.handler(testmsg) + assert mock_log.error.call_count == 1 + mock_log.error.assert_called_with("Couldn't find build notapackage-2.0-1.fc17 in DB") + + @mock.patch('bodhi.server.consumers.util.log') + def test_waiverdb_bodhi_message_no_updateid(self, mock_log): + """ + Assert that the consumer logs and returns if a Bodhi waiver + message is missing the update ID. + """ + testmsg = self.get_sample_message(typ="bodhi_update") + del testmsg.body["subject"]["item"] + self.handler(testmsg) + assert mock_log.error.call_count == 1 + mock_log.error.assert_called_with(f"Couldn't find update ID in message {testmsg.id}") + + @mock.patch('bodhi.server.consumers.util.log') + def test_waiverdb_bodhi_message_wrong_updateid(self, mock_log): + """ + Assert that the consumer raise an exception if we could not find the + update ID in the DB. + """ + testmsg = self.get_sample_message(typ="bodhi_update") + testmsg.body["subject"]["item"] = "NOTANUPDATE" + self.handler(testmsg) + assert mock_log.error.call_count == 1 + mock_log.error.assert_called_with("Couldn't find update NOTANUPDATE in DB")