Repository: fedora-infra/bodhi

Issue:
[M] Changes to test_gating_status should place comments on the update
We should leave comments on updates whenever their ```test_gating_status``` attribute is changed. This is important so that packagers get notifications when their tests fail.


Discussion:

Resolving patch:
diff --git a/bodhi/server/models.py b/bodhi/server/models.py
--- a/bodhi/server/models.py
+++ b/bodhi/server/models.py
@@ -3545,6 +3545,33 @@ def __json__(self, request=None, anonymize=False):
 
         return result
 
+    @staticmethod
+    def comment_on_test_gating_status_change(target, value, old, initiator):
+        """
+        Place comment on the update when ``test_gating_status`` changes.
+
+        Args:
+            target (Update): The compose that has had a change to its state attribute.
+            value (EnumSymbol): The new value of the test_gating_status.
+            old (EnumSymbol): The old value of the test_gating_status
+            initiator (sqlalchemy.orm.attributes.Event): The event object that is initiating this
+                transition.
+        """
+        if value != old:
+            target.comment(
+                Session(),
+                f"This update test gating status has been changed to '{value}'.",
+                author=u"bodhi",
+            )
+
+
+event.listen(
+    Update.test_gating_status,
+    'set',
+    Update.comment_on_test_gating_status_change,
+    active_history=True,
+)
+
 
 class Compose(Base):
     """


Test patch:
diff --git a/bodhi/tests/server/__init__.py b/bodhi/tests/server/__init__.py
--- a/bodhi/tests/server/__init__.py
+++ b/bodhi/tests/server/__init__.py
@@ -24,7 +24,7 @@
 
 from bodhi.server.models import (
     Bug, BuildrootOverride, Comment, Group, RpmPackage, Release, ReleaseState, RpmBuild,
-    Update, TestGatingStatus, UpdateRequest, UpdateSeverity, UpdateType, User, TestCase)
+    Update, UpdateRequest, UpdateSeverity, UpdateType, User, TestCase)
 
 
 def create_update(session, build_nvrs, release_name=u'F17'):
@@ -75,11 +75,8 @@ def create_update(session, build_nvrs, release_name=u'F17'):
     update = Update(
         title=', '.join(build_nvrs), builds=builds, user=user, request=UpdateRequest.testing,
         notes=u'Useful details!', type=UpdateType.bugfix, date_submitted=datetime(1984, 11, 2),
-        requirements=u'rpmlint', stable_karma=3, unstable_karma=-3,
-        test_gating_status=TestGatingStatus.passed)
+        requirements=u'rpmlint', stable_karma=3, unstable_karma=-3, release=release)
     session.add(update)
-    update.release = release
-
     return update
 
 
diff --git a/bodhi/tests/server/consumers/test_composer.py b/bodhi/tests/server/consumers/test_composer.py
--- a/bodhi/tests/server/consumers/test_composer.py
+++ b/bodhi/tests/server/consumers/test_composer.py
@@ -928,13 +928,20 @@ def test_security_update_priority(self, publish, *args):
                 builds=[build], user=user,
                 status=UpdateStatus.testing,
                 request=UpdateRequest.stable,
-                notes=u'Useful details!', release=release,
-                test_gating_status=TestGatingStatus.passed,
+                notes=u'Useful details!',
+                release=release,
                 stable_karma=3,
-                unstable_karma=-3)
-            update.type = UpdateType.security
+                unstable_karma=-3,
+                type=UpdateType.security)
+
+            update.test_gating_status = TestGatingStatus.passed
+
             db.add(update)
 
+            # Changing update's test_gating_status calls notifications.publish,
+            # so we have to reset mock calls
+            publish.reset_mock()
+
             # Wipe out the tag cache so it picks up our new release
             Release._tag_cache = None
 
@@ -1020,11 +1027,18 @@ def test_security_update_priority_testing(self, publish, *args):
                 builds=[build], user=user,
                 status=UpdateStatus.testing,
                 request=UpdateRequest.stable,
-                notes=u'Useful details!', release=release,
-                test_gating_status=TestGatingStatus.passed,
                 stable_karma=3,
-                unstable_karma=-3)
-            update.type = UpdateType.enhancement
+                unstable_karma=-3,
+                notes=u'Useful details!',
+                release=release,
+                type=UpdateType.enhancement)
+
+            update.test_gating_status = TestGatingStatus.passed
+
+            # Changing update's test_gating_status calls notifications.publish,
+            # so we have to reset mock calls
+            publish.reset_mock()
+
             db.add(update)
 
             # Wipe out the tag cache so it picks up our new release
@@ -1104,11 +1118,14 @@ def test_security_updates_parallel(self, publish, *args):
                 builds=[build], user=user,
                 status=UpdateStatus.testing,
                 request=UpdateRequest.stable,
-                notes=u'Useful details!', release=release,
-                test_gating_status=TestGatingStatus.passed,
                 stable_karma=3,
-                unstable_karma=-3)
-            update.type = UpdateType.security
+                unstable_karma=-3,
+                notes=u'Useful details!',
+                release=release,
+                type=UpdateType.security)
+
+            update.test_gating_status = TestGatingStatus.passed
+
             db.add(update)
 
             # Wipe out the tag cache so it picks up our new release
@@ -1530,11 +1547,14 @@ def test_compose_module(self, publish, *args):
                 builds=[build2], user=user,
                 status=UpdateStatus.testing,
                 request=UpdateRequest.stable,
-                notes=u'Useful details!', release=release,
-                test_gating_status=TestGatingStatus.passed,
                 stable_karma=3,
-                unstable_karma=-3)
-            update.type = UpdateType.security
+                unstable_karma=-3,
+                notes=u'Useful details!',
+                release=release,
+                type=UpdateType.security)
+
+            update.test_gating_status = TestGatingStatus.passed
+
             db.add(update)
 
             # Wipe out the tag cache so it picks up our new release
@@ -2327,11 +2347,14 @@ def setUp(self):
             builds=[build1, build2], user=user,
             status=UpdateStatus.pending,
             request=UpdateRequest.testing,
-            notes=u'Neat I can compose containers now', release=release,
-            test_gating_status=TestGatingStatus.passed,
             stable_karma=3,
-            unstable_karma=-3)
-        update.type = UpdateType.bugfix
+            unstable_karma=-3,
+            notes=u'Neat I can compose containers now',
+            release=release,
+            type=UpdateType.bugfix)
+
+        update.test_gating_status = TestGatingStatus.passed
+
         self.db.add(update)
         # Wipe out the tag cache so it picks up our new release
         Release._tag_cache = None
@@ -2495,11 +2518,14 @@ def setUp(self):
             builds=[build1, build2], user=user,
             status=UpdateStatus.pending,
             request=UpdateRequest.testing,
-            notes=u'Neat I can compose flatpaks now', release=release,
-            test_gating_status=TestGatingStatus.passed,
             stable_karma=3,
-            unstable_karma=-3)
-        update.type = UpdateType.bugfix
+            unstable_karma=-3,
+            notes=u'Neat I can compose flatpaks now',
+            release=release,
+            type=UpdateType.bugfix)
+
+        update.test_gating_status = TestGatingStatus.passed
+
         self.db.add(update)
         # Wipe out the tag cache so it picks up our new release
         Release._tag_cache = None
diff --git a/bodhi/tests/server/scripts/test_check_policies.py b/bodhi/tests/server/scripts/test_check_policies.py
--- a/bodhi/tests/server/scripts/test_check_policies.py
+++ b/bodhi/tests/server/scripts/test_check_policies.py
@@ -125,6 +125,9 @@ def test_policies_unsatisfied(self):
             update = self.db.query(models.Update).filter(models.Update.id == update.id).one()
             self.assertEqual(update.test_gating_status, models.TestGatingStatus.failed)
             self.assertEqual(update.greenwave_summary_string, '1 of 2 tests are failed')
+            # Check for the comment
+            expected_comment = u"This update test gating status has been changed to 'failed'."
+            self.assertEqual(update.comments[-1].text, expected_comment)
 
         expected_query = {
             'product_version': 'fedora-17', 'decision_context': 'bodhi_update_push_stable',
@@ -203,6 +206,9 @@ def test_pushed_update(self):
         }
         mock_greenwave.assert_called_once_with(config['greenwave_api_url'] + '/decision',
                                                expected_query)
+        # Check for the comment
+        expected_comment = u"This update test gating status has been changed to 'failed'."
+        self.assertEqual(update.comments[-1].text, expected_comment)
 
     @patch.dict(config, [('greenwave_api_url', 'http://domain.local')])
     def test_unrestricted_policy(self):
@@ -223,6 +229,9 @@ def test_unrestricted_policy(self):
             update = self.db.query(models.Update).filter(models.Update.id == update.id).one()
             self.assertEqual(update.test_gating_status, models.TestGatingStatus.ignored)
             self.assertEqual(update.greenwave_summary_string, 'no tests are required')
+            # Check for the comment
+            expected_comment = u"This update test gating status has been changed to 'ignored'."
+            self.assertEqual(update.comments[-1].text, expected_comment)
 
         expected_query = {
             'product_version': 'fedora-17', 'decision_context': 'bodhi_update_push_stable',
diff --git a/bodhi/tests/server/services/test_overrides.py b/bodhi/tests/server/services/test_overrides.py
--- a/bodhi/tests/server/services/test_overrides.py
+++ b/bodhi/tests/server/services/test_overrides.py
@@ -329,6 +329,8 @@ def test_create_override_for_build_with_test_gating_status_failed(self, publish)
         self.db.add(build)
         self.db.flush()
 
+        publish.reset_mock()
+
         expiration_date = datetime.utcnow() + timedelta(days=1)
 
         data = {'nvr': build.nvr, 'notes': u'blah blah blah',
diff --git a/bodhi/tests/server/services/test_updates.py b/bodhi/tests/server/services/test_updates.py
--- a/bodhi/tests/server/services/test_updates.py
+++ b/bodhi/tests/server/services/test_updates.py
@@ -5234,6 +5234,10 @@ def test_waive_test_results_1_unsatisfied_requirement(
         up = self.db.query(Update).filter_by(title=nvr).one()
         # The test gating status should have been reset to waiting.
         self.assertEqual(up.test_gating_status, TestGatingStatus.waiting)
+        # Check for the comment multiple ways
+        expected_comment = u"This update test gating status has been changed to 'waiting'."
+        self.assertEqual(res.json_body["update"]['comments'][-1]['text'], expected_comment)
+        self.assertEqual(up.comments[-1].text, expected_comment)
 
     @mock.patch.dict(config, [('test_gating.required', True)])
     @mock.patch('bodhi.server.util.waiverdb_api_post')
@@ -5324,6 +5328,10 @@ def test_waive_test_results_2_unsatisfied_requirements(
         up = self.db.query(Update).filter_by(title=nvr).one()
         # The test gating status should have been reset to waiting.
         self.assertEqual(up.test_gating_status, TestGatingStatus.waiting)
+        # Check for the comment multiple ways
+        expected_comment = u"This update test gating status has been changed to 'waiting'."
+        self.assertEqual(res.json_body["update"]['comments'][-1]['text'], expected_comment)
+        self.assertEqual(up.comments[-1].text, expected_comment)
 
     @mock.patch.dict(config, [('test_gating.required', True)])
     @mock.patch('bodhi.server.util.waiverdb_api_post')
@@ -5402,6 +5410,10 @@ def test_waive_test_results_1_of_2_unsatisfied_requirements(
         up = self.db.query(Update).filter_by(title=nvr).one()
         # The test gating status should have been reset to waiting.
         self.assertEqual(up.test_gating_status, TestGatingStatus.waiting)
+        # Check for the comment multiple ways
+        expected_comment = u"This update test gating status has been changed to 'waiting'."
+        self.assertEqual(res.json_body["update"]['comments'][-1]['text'], expected_comment)
+        self.assertEqual(up.comments[-1].text, expected_comment)
 
     @mock.patch.dict(config, [('test_gating.required', True)])
     @mock.patch('bodhi.server.util.waiverdb_api_post')
@@ -5496,6 +5508,10 @@ def test_waive_test_results_2_of_2_unsatisfied_requirements(
         up = self.db.query(Update).filter_by(title=nvr).one()
         # The test gating status should have been updated to waiting.
         self.assertEqual(up.test_gating_status, TestGatingStatus.waiting)
+        # Check for the comment multiple ways
+        expected_comment = u"This update test gating status has been changed to 'waiting'."
+        self.assertEqual(res.json_body["update"]['comments'][-1]['text'], expected_comment)
+        self.assertEqual(up.comments[-1].text, expected_comment)
 
     @mock.patch.dict(config, [('test_gating.required', True)])
     @mock.patch('bodhi.server.util.waiverdb_api_post')
diff --git a/bodhi/tests/server/test_models.py b/bodhi/tests/server/test_models.py
--- a/bodhi/tests/server/test_models.py
+++ b/bodhi/tests/server/test_models.py
@@ -1925,8 +1925,7 @@ class TestUpdate(ModelTest):
         stable_karma=3,
         unstable_karma=-3,
         close_bugs=True,
-        notes=u'foobar',
-        test_gating_status=TestGatingStatus.passed)
+        notes=u'foobar')
 
     @staticmethod
     def do_get_dependencies():
@@ -3661,9 +3660,9 @@ def test_validate_release_failure(self):
             request=UpdateRequest.stable,
             type=UpdateType.enhancement,
             notes=u'Useful details!',
-            test_gating_status=TestGatingStatus.passed,
             stable_karma=3,
             unstable_karma=-3)
+
         update.release = release
         self.db.add(update)
         self.db.flush()
@@ -3714,8 +3713,9 @@ def test_validate_release_success(self):
             builds=[build1], user=user,
             status=UpdateStatus.testing,
             request=UpdateRequest.stable,
-            notes=u'Useful details!', release=release,
-            test_gating_status=TestGatingStatus.passed)
+            notes=u'Useful details!',
+            release=release)
+
         self.db.add(update1)
 
         # This should not raise an Exception.
@@ -3724,8 +3724,9 @@ def test_validate_release_success(self):
             builds=[build2], user=user,
             status=UpdateStatus.testing,
             request=UpdateRequest.stable,
-            notes=u'Useful details!', release=release,
-            test_gating_status=TestGatingStatus.passed)
+            notes=u'Useful details!',
+            release=release)
+
         self.db.add(update2)
 
         self.assertEqual(update2.release, release)
@@ -3781,6 +3782,10 @@ def test_can_waive_multiple_test_results_of_an_update(self, post, greenwave_api_
                               'waiverdb.access_token': 'abc'}):
             self.obj.waive_test_results('foo', 'this is not true!')
 
+        # Check for the comment
+        expected_comment = u"This update test gating status has been changed to 'waiting'."
+        self.assertEqual(self.obj.comments[-1].text, expected_comment)
+
         expected_calls = []
         for test in ('dist.depcheck', 'dist.rpmdeplint', 'dist.someothertest'):
             data = {
@@ -3845,6 +3850,28 @@ def test_can_waive_test_results_of_an_update(self, mock_waiverdb, mock_greenwave
             mock_waiverdb.assert_called_once_with(
                 '{}/waivers/'.format(config.get('waiverdb_api_url')), wdata)
 
+        # Check for the comment
+        expected_comment = u"This update test gating status has been changed to 'waiting'."
+        self.assertEqual(update.comments[-1].text, expected_comment)
+
+    def test_comment_on_test_gating_status_change(self):
+        """Assert that Bodhi will leave comment only when test_gating_status changes."""
+        # Let's make sure that update has no comments.
+        self.assertEqual(len(self.obj.comments), 0)
+
+        self.obj.test_gating_status = TestGatingStatus.waiting
+
+        # Check for the comment about test_gating_status change
+        expected_comment = u"This update test gating status has been changed to 'waiting'."
+        self.assertEqual(self.obj.comments[0].text, expected_comment)
+        self.assertEqual(len(self.obj.comments), 1)
+
+        # Let's set test_gating_status to 'waiting' once again.
+        self.obj.test_gating_status = TestGatingStatus.waiting
+
+        # We should have still only one comment about test_gating_status change.
+        self.assertEqual(len(self.obj.comments), 1)
+
 
 class TestUser(ModelTest):
     klass = model.User