Repository: fedora-infra/bodhi

Issue:
Create single build updates in pending status for rawhide
Now that we have the automation in place to automatically move an update from pending to testing we should use that for single build updates too.

For that we need to change the automatic_update consumer (https://github.com/fedora-infra/bodhi/blob/develop/bodhi/server/consumers/automatic_updates.py) to create the update in the pending status. 

And make sure that the signed consumer will move the updates from pending to testing when the build is signed.


Discussion:
When we move this to production, we'll have to be careful to adjust robotsignatory's config and to double check that we do not leave any builds in a middle-state where they no longer move forward.
We'll need adjust our docs as well (I can pick this up though)

Resolving patch:
diff --git a/bodhi/server/consumers/automatic_updates.py b/bodhi/server/consumers/automatic_updates.py
--- a/bodhi/server/consumers/automatic_updates.py
+++ b/bodhi/server/consumers/automatic_updates.py
@@ -28,7 +28,7 @@
 
 from bodhi.server import buildsys
 from bodhi.server.config import config
-from bodhi.server.models import Build, ContentType, Package, Release, TestGatingStatus
+from bodhi.server.models import Build, ContentType, Package, Release
 from bodhi.server.models import Update, UpdateStatus, UpdateType, User
 from bodhi.server.util import transactional_session_maker
 
@@ -105,7 +105,7 @@ def __call__(self, message: fedora_messaging.api.Message) -> None:
 
         with self.db_factory() as dbsession:
             rel = dbsession.query(Release).filter_by(create_automatic_updates=True,
-                                                     pending_testing_tag=btag).first()
+                                                     candidate_tag=btag).first()
             if not rel:
                 log.debug(f"Ignoring build being tagged into {btag!r}, no release configured for "
                           "automatic updates for it found.")
@@ -114,22 +114,7 @@ def __call__(self, message: fedora_messaging.api.Message) -> None:
             bcls = ContentType.infer_content_class(Build, kbuildinfo)
             build = bcls.get(bnvr)
             if build and build.update:
-                if build.update.status == UpdateStatus.pending:
-                    log.info(
-                        f"Build, active update for {bnvr} exists already "
-                        "in Pending, moving it along.")
-                    build.update.status = UpdateStatus.testing
-                    build.update.request = None
-                    dbsession.add(build)
-                    if config.get('test_gating.required'):
-                        log.debug(
-                            'Test gating is required, marking the update as waiting on test '
-                            'gating and updating it from Greenwave to get the real status.')
-                        build.update.test_gating_status = TestGatingStatus.waiting
-                        build.update.update_test_gating_status()
-                    dbsession.commit()
-                else:
-                    log.info(f"Build, active update for {bnvr} exists already, skipping.")
+                log.info(f"Build, active update for {bnvr} exists already, skipping.")
                 return
 
             if not build:
@@ -163,7 +148,7 @@ def __call__(self, message: fedora_messaging.api.Message) -> None:
                 unstable_karma=-3,
                 autokarma=False,
                 user=user,
-                status=UpdateStatus.testing,
+                status=UpdateStatus.pending,
             )
 
             # Comment on the update that it was automatically created.
@@ -173,14 +158,6 @@ def __call__(self, message: fedora_messaging.api.Message) -> None:
                 author="bodhi",
             )
 
-            if config.get('test_gating.required'):
-                log.debug(
-                    'Test gating required is enforced, marking the update as '
-                    'waiting on test gating and updating it from Greenwave to '
-                    'get the real status.')
-                update.test_gating_status = TestGatingStatus.waiting
-                update.update_test_gating_status()
-
             log.debug("Adding new update to the database.")
             dbsession.add(update)
 
diff --git a/bodhi/server/consumers/signed.py b/bodhi/server/consumers/signed.py
--- a/bodhi/server/consumers/signed.py
+++ b/bodhi/server/consumers/signed.py
@@ -27,7 +27,8 @@
 import fedora_messaging
 from sqlalchemy import func
 
-from bodhi.server.models import Build, UpdateStatus
+from bodhi.server.config import config
+from bodhi.server.models import Build, UpdateStatus, TestGatingStatus
 from bodhi.server.util import transactional_session_maker
 
 log = logging.getLogger('bodhi')
@@ -110,10 +111,17 @@ def __call__(self, message: fedora_messaging.api.Message):
             log.info("Build %s has been marked as signed" % build_nvr)
 
             # If every build in update is signed change status to testing
-            if build.update.from_tag and build.update.signed():
+            if not build.update.release.composed_by_bodhi and build.update.signed():
                 log.info("Every build in update is signed, set status to testing")
 
                 build.update.status = UpdateStatus.testing
                 build.update.date_testing = func.current_timestamp()
                 build.update.request = None
+
+                if config.get("test_gating.required"):
+                    log.debug('Test gating is required, marking the update as waiting on test '
+                              'gating and updating it from Greenwave to get the real status.')
+                    build.update.test_gating_status = TestGatingStatus.waiting
+                    build.update.update_test_gating_status()
+
                 log.info(f"Update {build.update.display_name} status has been set to testing")


Test patch:
diff --git a/bodhi/tests/server/consumers/test_automatic_updates.py b/bodhi/tests/server/consumers/test_automatic_updates.py
--- a/bodhi/tests/server/consumers/test_automatic_updates.py
+++ b/bodhi/tests/server/consumers/test_automatic_updates.py
@@ -25,11 +25,10 @@
 from fedora_messaging.testing import mock_sends
 import pytest
 
-from bodhi.messages.schemas import update as update_schemas
 from bodhi.server.config import config
 from bodhi.server.consumers.automatic_updates import AutomaticUpdateHandler
 from bodhi.server.models import (
-    Build, Release, TestGatingStatus, Update, UpdateRequest, UpdateStatus, UpdateType, User
+    Build, Release, Update, UpdateRequest, UpdateStatus, UpdateType, User
 )
 from bodhi.tests.server import base
 
@@ -53,7 +52,7 @@ def setup_method(self, method):
             'name': 'colord',
             'tag_id': 214,
             'instance': 's390',
-            'tag': 'f17-updates-testing-pending',
+            'tag': 'f17-updates-candidate',
             'user': 'sharkcz',
             'version': '1.3.4',
             'owner': 'sharkcz',
@@ -73,8 +72,7 @@ def test_consume(self, caplog):
         caplog.set_level(logging.DEBUG)
 
         # process the message
-        with mock_sends(update_schemas.UpdateReadyForTestingV1):
-            self.handler(self.sample_message)
+        self.handler(self.sample_message)
 
         # check if the update exists...
         update = self.db.query(Update).filter(
@@ -84,7 +82,7 @@ def test_consume(self, caplog):
         # ...and some of its properties
         assert update is not None
         assert update.type == UpdateType.unspecified
-        assert update.status == UpdateStatus.testing
+        assert update.status == UpdateStatus.pending
         assert update.autokarma == False
         assert update.test_gating_status is None
 
@@ -102,8 +100,7 @@ def test_consume_with_orphan_build(self, caplog):
         caplog.set_level(logging.DEBUG)
 
         # Run the handler to create the build & update, then remove the update.
-        with mock_sends(update_schemas.UpdateReadyForTestingV1):
-            self.handler(self.sample_message)
+        self.handler(self.sample_message)
         build = self.db.query(Build).filter_by(nvr=self.sample_nvr).one()
         update = build.update
         build.update = None  # satisfy foreign key constraint
@@ -111,8 +108,7 @@ def test_consume_with_orphan_build(self, caplog):
 
         # Now test with the same message again which should encounter the
         # build already existing in the database.
-        with mock_sends(update_schemas.UpdateReadyForTestingV1):
-            self.handler(self.sample_message)
+        self.handler(self.sample_message)
 
         # check if the update exists...
         update = self.db.query(Update).filter(
@@ -122,7 +118,7 @@ def test_consume_with_orphan_build(self, caplog):
         # ...and some of its properties
         assert update is not None
         assert update.type == UpdateType.unspecified
-        assert update.status == UpdateStatus.testing
+        assert update.status == UpdateStatus.pending
         assert update.test_gating_status is None
 
         expected_username = base.buildsys.DevBuildsys._build_data['owner_name']
@@ -130,91 +126,16 @@ def test_consume_with_orphan_build(self, caplog):
 
         assert not any(r.levelno >= logging.WARNING for r in caplog.records)
 
-    @mock.patch.dict(config, [('test_gating.required', True),
-                              ('greenwave_api_url', 'http://domain.local')])
-    @pytest.mark.parametrize('gated', (True, False, 'error'))
-    def test_consume_with_gating(self, caplog, gated):
-        """Assert that messages about tagged builds create an update with the expected gating status.
-        """
-        caplog.set_level(logging.DEBUG)
-
-        # process the message
-        with mock.patch('bodhi.server.models.util.greenwave_api_post') as mock_greenwave:
-            if gated == 'error':
-                mock_greenwave.side_effect = RuntimeError("Boo!")
-            else:
-                if gated:
-                    greenwave_response = {
-                        'policies_satisfied': False,
-                        'summary': "1 of 1 required test results missing",
-                    }
-                else:
-                    greenwave_response = {
-                        'policies_satisfied': True,
-                        'summary': "no tests are required",
-                    }
-                mock_greenwave.return_value = greenwave_response
-            with mock_sends(update_schemas.UpdateReadyForTestingV1):
-                self.handler(self.sample_message)
-
-        # check if the update exists...
-        update = self.db.query(Update).filter(
-            Update.builds.any(Build.nvr == self.sample_nvr)
-        ).first()
-
-        # ...and some of its properties
-        assert update is not None
-        assert update.type == UpdateType.unspecified
-        assert update.status == UpdateStatus.testing
-        if gated == 'error':
-            assert update.test_gating_status == TestGatingStatus.greenwave_failed
-        elif gated:
-            assert update.test_gating_status == TestGatingStatus.failed
-        else:
-            assert update.test_gating_status == TestGatingStatus.ignored
-
-        expected_username = base.buildsys.DevBuildsys._build_data['owner_name']
-        assert update.user and update.user.name == expected_username
-
-        # check comments and their order
-        if gated == 'error':
-            final_status = 'greenwave_failed'
-        elif gated:
-            final_status = 'failed'
-        else:
-            final_status = 'ignored'
-
-        expected_comments = [
-            "This update was automatically created",
-            "This update's test gating status has been changed to 'waiting'.",
-            f"This update's test gating status has been changed to '{final_status}'.",
-        ]
-
-        assert (expected_comments == [c.text for c in update.comments])
-
-        # check for log records, warning or higher
-        if gated == 'error':
-            warn_higher_records = [r for r in caplog.records if r.levelno >= logging.WARNING]
-            assert len(warn_higher_records) == 1
-            log_record = warn_higher_records[0]
-            assert log_record.levelno == logging.ERROR
-            assert log_record.message == "Boo!"
-        else:
-            assert not any(r.levelno >= logging.WARNING for r in caplog.records)
-
     def test_existing_pending_update(self, caplog):
         """
-        Ensure an update is moved to testing if a matching pending one exists.
+        Ensure an update is not created if a matching pending one exists.
         """
         caplog.set_level(logging.DEBUG)
 
-        with mock_sends(update_schemas.UpdateReadyForTestingV1):
-            self.handler(self.sample_message)
+        self.handler(self.sample_message)
         update = self.db.query(Update).filter(
             Update.builds.any(Build.nvr == self.sample_nvr)
         ).first()
-        # Check it was created in testing
-        assert update.status == UpdateStatus.testing
         # Move it back to Pending as if the user has manually created it
         update.status = UpdateStatus.pending
         update.request = UpdateRequest.testing
@@ -225,106 +146,10 @@ def test_existing_pending_update(self, caplog):
 
         caplog.clear()
 
-        with mock_sends(update_schemas.UpdateReadyForTestingV1):
-            self.handler(self.sample_message)
-
-        assert (f"Build, active update for {self.sample_nvr} exists already in "
-                "Pending, moving it along." in caplog.messages)
-
-        update = self.db.query(Update).filter(
-            Update.builds.any(Build.nvr == self.sample_nvr)
-        ).first()
-        assert update.status == UpdateStatus.testing
-        assert update.request is None
-
-    @mock.patch.dict(config, [('test_gating.required', True),
-                              ('greenwave_api_url', 'http://domain.local')])
-    @pytest.mark.parametrize('gated', (True, False, 'error'))
-    def test_existing_pending_update_with_gating(self, caplog, gated):
-        """
-        Ensure an update is moved to testing if a matching pending one exists.
-
-        Contrary to test_existing_pending_update(), this test runs with test
-        gating required, i.e. simulates querying Greenwave for the gating
-        status.
-        """
-        def _call_handler():
-            """ Call the handler to process the sample message. """
-            with mock.patch('bodhi.server.models.util.greenwave_api_post') as mock_greenwave:
-                if gated == 'error':
-                    mock_greenwave.side_effect = RuntimeError("Boo!")
-                else:
-                    if gated:
-                        greenwave_response = {
-                            'policies_satisfied': False,
-                            'summary': "1 of 1 required test results missing",
-                        }
-                    else:
-                        greenwave_response = {
-                            'policies_satisfied': True,
-                            'summary': "no tests are required",
-                        }
-                    mock_greenwave.return_value = greenwave_response
-                with mock_sends(update_schemas.UpdateReadyForTestingV1):
-                    self.handler(self.sample_message)
-
-                # Quick sanity checks
-                assert mock_greenwave.call_count == 1
-
-            update = self.db.query(Update).filter(
-                Update.builds.any(Build.nvr == self.sample_nvr)
-            ).first()
-            if gated == 'error':
-                assert update.test_gating_status == TestGatingStatus.greenwave_failed
-            elif gated:
-                assert update.test_gating_status == TestGatingStatus.failed
-            else:
-                assert update.test_gating_status == TestGatingStatus.ignored
-            return update
-
-        # First run
-        caplog.set_level(logging.DEBUG)
-        update = _call_handler()
-
-        # Double-check how it was created in the first run
-        assert update.status == UpdateStatus.testing
-
-        # Move it back to Pending as if the user had manually created it
-        update.status = UpdateStatus.pending
-        update.request = UpdateRequest.testing
-        self.db.add(update)
-        self.db.flush()
-        # Clear pending messages
-        self.db.info['messages'] = []
-
-        caplog.clear()
-
-        # Second run
-        update = _call_handler()
-
-        assert (f"Build, active update for {self.sample_nvr} exists already in "
-                "Pending, moving it along." in caplog.messages)
-
-        assert update.status == UpdateStatus.testing
-        assert update.request is None
-
-        # check comments and their order
-        if gated == 'error':
-            final_status = 'greenwave_failed'
-        elif gated:
-            final_status = 'failed'
-        else:
-            final_status = 'ignored'
-
-        expected_comments = [
-            "This update was automatically created",
-            "This update's test gating status has been changed to 'waiting'.",
-            f"This update's test gating status has been changed to '{final_status}'.",
-            "This update's test gating status has been changed to 'waiting'.",
-            f"This update's test gating status has been changed to '{final_status}'.",
-        ]
+        self.handler(self.sample_message)
 
-        assert (expected_comments == [c.text for c in update.comments])
+        assert (f"Build, active update for {self.sample_nvr} exists already, skipping."
+                in caplog.messages)
 
     # The following tests cover lesser-travelled code paths.
 
@@ -392,8 +217,7 @@ def test_missing_user(self, caplog):
         self.db.query(User).filter_by(name=expected_username).delete()
         self.db.flush()
 
-        with mock_sends(update_schemas.UpdateReadyForTestingV1):
-            self.handler(self.sample_message)
+        self.handler(self.sample_message)
 
         assert(f"Creating bodhi user for '{expected_username}'."
                in caplog.messages)
@@ -412,8 +236,7 @@ def test_existing_user(self, caplog):
         self.db.flush()
 
         with mock.patch('bodhi.server.models.handle_update'):
-            with mock_sends(update_schemas.UpdateReadyForTestingV1):
-                self.handler(self.sample_message)
+            self.handler(self.sample_message)
 
         assert(f"Creating bodhi user for '{expected_username}'."
                not in caplog.messages)
@@ -437,8 +260,7 @@ def test_duplicate_message(self, caplog):
         """Assert that duplicate messages ignore existing build/update."""
         caplog.set_level(logging.DEBUG)
 
-        with mock_sends(update_schemas.UpdateReadyForTestingV1):
-            self.handler(self.sample_message)
+        self.handler(self.sample_message)
 
         caplog.clear()
 
diff --git a/bodhi/tests/server/consumers/test_signed.py b/bodhi/tests/server/consumers/test_signed.py
--- a/bodhi/tests/server/consumers/test_signed.py
+++ b/bodhi/tests/server/consumers/test_signed.py
@@ -18,18 +18,21 @@
 """This test suite contains tests for the bodhi.server.consumers.signed module."""
 
 from unittest import mock
-import unittest
 
 from fedora_messaging.api import Message
 
+from bodhi.server.config import config
 from bodhi.server.consumers import signed
-from bodhi.server.models import UpdateStatus
+from bodhi.server.models import UpdateStatus, TestGatingStatus
+from bodhi.tests.server import base
 
 
-class TestSignedHandlerConsume(unittest.TestCase):
+class TestSignedHandlerConsume(base.BasePyTestCase):
     """Test class for the :func:`SignedHandler.consume` method."""
 
-    def setUp(self):
+    def setup_method(self, method):
+        super().setup_method(method)
+
         self.sample_message = Message(
             topic='',
             body={
@@ -71,7 +74,7 @@ def test_consume(self, mock_build_model):
         build.release.pending_testing_tag = self.sample_message.body["tag"]
 
         self.handler(self.sample_message)
-        self.assertTrue(build.signed is True)
+        assert build.signed is True
 
     @mock.patch('bodhi.server.consumers.signed.Build')
     def test_consume_not_pending_testing_tag(self, mock_build_model):
@@ -86,7 +89,7 @@ def test_consume_not_pending_testing_tag(self, mock_build_model):
         build.release.pending_testing_tag = "some tag that isn't pending testing"
 
         self.handler(self.sample_message)
-        self.assertFalse(build.signed is True)
+        assert build.signed is False
 
     @mock.patch('bodhi.server.consumers.signed.Build')
     def test_consume_no_release(self, mock_build_model):
@@ -101,7 +104,7 @@ def test_consume_no_release(self, mock_build_model):
         build.release = None
 
         self.handler(self.sample_message)
-        self.assertFalse(build.signed is True)
+        assert build.signed is False
 
     @mock.patch('bodhi.server.consumers.signed.log')
     @mock.patch('bodhi.server.consumers.signed.Build')
@@ -126,7 +129,7 @@ def test_consume_duplicate(self, mock_build_model, mock_log):
         self.handler(self.sample_message)
         mock_log.info.assert_called_with(
             "Build was already marked as signed (maybe a duplicate message)")
-        self.assertEqual(mock_log.info.call_count, 2)
+        assert mock_log.info.call_count == 2
 
     @mock.patch('bodhi.server.consumers.signed.log')
     @mock.patch('bodhi.server.consumers.signed.Build')
@@ -145,7 +148,7 @@ def test_consume_from_tag_wrong_tag(self, mock_build_model, mock_log):
         self.handler(self.sample_side_tag_message)
         mock_log.info.assert_called_with(
             "Tag is not testing side tag, skipping")
-        self.assertEqual(mock_log.info.call_count, 2)
+        assert mock_log.info.call_count == 2
 
     @mock.patch('bodhi.server.consumers.signed.Build')
     def test_consume_from_tag_not_signed(self, mock_build_model):
@@ -164,10 +167,11 @@ def test_consume_from_tag_not_signed(self, mock_build_model):
         build.update = update
 
         self.handler(self.sample_side_tag_message)
-        self.assertEqual(build.signed, True)
-        self.assertEqual(update.status, UpdateStatus.pending)
+        assert build.signed is True
+        assert update.status == UpdateStatus.pending
 
     @mock.patch('bodhi.server.consumers.signed.Build')
+    @mock.patch.dict(config, [('test_gating.required', True)])
     def test_consume_from_tag(self, mock_build_model):
         """
         Assert that update created from tag is handled correctly when message
@@ -182,9 +186,12 @@ def test_consume_from_tag(self, mock_build_model):
         update.from_tag = "f30-side-tag"
         update.signed.return_value = True
         update.status = UpdateStatus.pending
+        update.release.composed_by_bodhi = False
+        update.test_gating_status = None
         build.update = update
 
         self.handler(self.sample_side_tag_message)
-        self.assertEqual(build.signed, True)
-        self.assertEqual(build.update.request, None)
-        self.assertEqual(update.status, UpdateStatus.testing)
+        assert build.signed is True
+        assert build.update.request is None
+        assert update.status == UpdateStatus.testing
+        assert update.test_gating_status == TestGatingStatus.waiting