From 110b7c94aa18e9d9eea03ccb7c27f7a800d7ed12 Mon Sep 17 00:00:00 2001
From: Ralph Bean <rbean@redhat.com>
Date: Nov 08 2017 20:28:32 +0000
Subject: [PATCH 1/2] Test decision changes for compose results.


---

diff --git a/functional-tests/conftest.py b/functional-tests/conftest.py
index f3be25a..6e00916 100644
--- a/functional-tests/conftest.py
+++ b/functional-tests/conftest.py
@@ -126,6 +126,21 @@ class TestDataBuilder(object):
     def unique_compose_id(self):
         return 'Fedora-9000-19700101.n.{}'.format(self._counter.next())
 
+    def create_compose_result(self, compose_id, testcase_name, outcome, scenario=None):
+        data = {
+            'testcase': {'name': testcase_name},
+            'data': {'productmd.compose.id': compose_id},
+            'outcome': outcome,
+        }
+        if scenario:
+            data['data']['scenario'] = scenario
+        response = self.requests_session.post(
+            self.resultsdb_url + 'api/v2.0/results',
+            headers={'Content-Type': 'application/json'},
+            data=json.dumps(data))
+        response.raise_for_status()
+        return response.json()
+
     def create_result(self, item, testcase_name, outcome, scenario=None):
         data = {
             'testcase': {'name': testcase_name},
diff --git a/functional-tests/consumers/test_resultsdb.py b/functional-tests/consumers/test_resultsdb.py
index 7816764..00b4aa7 100644
--- a/functional-tests/consumers/test_resultsdb.py
+++ b/functional-tests/consumers/test_resultsdb.py
@@ -341,3 +341,70 @@ def test_invalidate_new_result_with_no_preexisting_cache(
     ]
     handler.consume(message)
     handler.cache.delete.assert_not_called()
+
+
+@mock.patch('greenwave.consumers.resultsdb.fedmsg.config.load_config')
+@mock.patch('greenwave.consumers.resultsdb.fedmsg.publish')
+def test_consume_compose_id_result(
+        mock_fedmsg, load_config, requests_session, greenwave_server,
+        testdatabuilder, monkeypatch):
+    monkeypatch.setenv('TEST', 'true')
+    load_config.return_value = {'greenwave_api_url': greenwave_server.url + 'api/v1.0'}
+    compose_id = testdatabuilder.unique_compose_id()
+    result = testdatabuilder.create_compose_result(
+        compose_id=compose_id,
+        testcase_name='compose.install_no_user',
+        outcome='PASSED')
+    message = {
+        'body': {
+            'topic': 'taskotron.result.new',
+            'msg': {
+                'result': {
+                    'id': result['id'],
+                    'outcome': 'PASSED'
+                },
+                'task': {
+                    "productmd.compose.id": "Fedora-Rawhide-20171108.n.0",
+                    "name": "compose.install_no_user",
+                },
+            }
+        }
+    }
+    hub = mock.MagicMock()
+    hub.config = {
+        'environment': 'environment',
+        'topic_prefix': 'topic_prefix',
+        'greenwave_cache': {'backend': 'dogpile.cache.null'},
+    }
+    handler = resultsdb.ResultsDBHandler(hub)
+    assert handler.topic == ['topic_prefix.environment.taskotron.result.new']
+    handler.consume(message)
+
+    # get old decision
+    data = {
+        'decision_context': 'rawhide_compose_sync_to_mirrors',
+        'product_version': 'fedora-rawhide',
+        'subject': [{'productmd.compose.id': compose_id}],
+        'ignore_result': [result['id']]
+    }
+    r = requests_session.post(greenwave_server.url + 'api/v1.0/decision',
+                              headers={'Content-Type': 'application/json'},
+                              data=json.dumps(data))
+    assert r.status_code == 200
+    old_decision = r.json()
+    msg = {
+        'policies_satisfied': False,
+        'decision_context': 'rawhide_compose_sync_to_mirrors',
+        'product_version': 'fedora-rawhide',
+        'unsatisfied_requirements': [
+        ],
+        'summary': '2 of 3 required tests not found',
+        'subject': [
+            {
+                'productmd.compose.id': compose_id,
+            }
+        ],
+        'applicable_policies': ['blahblah'],
+        'previous': old_decision,
+    }
+    mock_fedmsg.assert_any_call(topic='decision.update', msg=msg)

From 6d461a57db6a81d4753a7c090afea53473a1e9b6 Mon Sep 17 00:00:00 2001
From: Ralph Bean <rbean@redhat.com>
Date: Nov 09 2017 15:29:43 +0000
Subject: [PATCH 2/2] Announce decisions about specified sets of subject keys.


Fixes #92, for now.

I ran into this while talking through how we would do compose gating with
@mohanboddu.

The issue is (as described in other terms in #92), that greenwave doesn't know
what to announce if the subject isn't in the form of a simple item/type pair.
Compose decisions have just such a subject.

Here, we add a list of "interesting subjects" to the configuration.  As results
come in off the bus, we look to see if any of them might match up with an
interesting subject.  If they do, then construct that subject, and ask
greenwave if a decision would change, and publish a message (as we have done
before).

Maintaining a list of interesting subjects indefinitely will be a chore.
Elsewhere, @dcallagh has proposed giving greenwave some state so that it can
record all of the subject queries it has ever received, and use that to
calculate its announcements.

---

diff --git a/functional-tests/consumers/test_resultsdb.py b/functional-tests/consumers/test_resultsdb.py
index 00b4aa7..a822cd7 100644
--- a/functional-tests/consumers/test_resultsdb.py
+++ b/functional-tests/consumers/test_resultsdb.py
@@ -214,7 +214,7 @@ def test_invalidate_new_result_with_mocked_cache(
     ]
     handler.consume(message)
     expected = ("greenwave.resources:retrieve_results|"
-                "{'item': '%s', 'type': 'koji_build'}" % nvr)
+                "{u'item': u'%s', u'type': u'koji_build'}" % nvr)
     handler.cache.delete.assert_called_once_with(expected)
 
 
@@ -354,6 +354,7 @@ def test_consume_compose_id_result(
     result = testdatabuilder.create_compose_result(
         compose_id=compose_id,
         testcase_name='compose.install_no_user',
+        scenario='scenario1',
         outcome='PASSED')
     message = {
         'body': {
@@ -364,7 +365,7 @@ def test_consume_compose_id_result(
                     'outcome': 'PASSED'
                 },
                 'task': {
-                    "productmd.compose.id": "Fedora-Rawhide-20171108.n.0",
+                    "productmd.compose.id": compose_id,
                     "name": "compose.install_no_user",
                 },
             }
@@ -393,18 +394,35 @@ def test_consume_compose_id_result(
     assert r.status_code == 200
     old_decision = r.json()
     msg = {
-        'policies_satisfied': False,
-        'decision_context': 'rawhide_compose_sync_to_mirrors',
+        u'applicable_policies': [u'openqa_important_stuff_for_rawhide'],
+        u'decision_context': u'rawhide_compose_sync_to_mirrors',
+        u'policies_satisfied': False,
         'product_version': 'fedora-rawhide',
-        'unsatisfied_requirements': [
-        ],
-        'summary': '2 of 3 required tests not found',
-        'subject': [
-            {
-                'productmd.compose.id': compose_id,
-            }
-        ],
-        'applicable_policies': ['blahblah'],
-        'previous': old_decision,
+        'subject': [{u'productmd.compose.id': 'Fedora-9000-19700101.n.18'}],
+        u'summary': u'1 of 2 required tests not found',
+        'previous': {
+            u'applicable_policies': [u'openqa_important_stuff_for_rawhide'],
+            u'policies_satisfied': False,
+            u'summary': u'no test results found',
+            u'unsatisfied_requirements': [
+                {u'item': {
+                    u'productmd.compose.id': compose_id},
+                    u'scenario': u'scenario1',
+                    u'testcase': u'compose.install_no_user',
+                    u'type': u'test-result-missing'},
+                {u'item': {
+                    u'productmd.compose.id': compose_id},
+                    u'scenario': u'scenario2',
+                    u'testcase': u'compose.install_no_user',
+                    u'type': u'test-result-missing'},
+            ]
+        },
+        u'unsatisfied_requirements': [{
+            u'item': {u'productmd.compose.id': compose_id},
+            u'scenario': u'scenario2',
+            u'testcase': u'compose.install_no_user',
+            u'type': u'test-result-missing'}
+        ]
     }
+
     mock_fedmsg.assert_any_call(topic='decision.update', msg=msg)
diff --git a/greenwave/config.py b/greenwave/config.py
index 58e51ae..4a1fe71 100644
--- a/greenwave/config.py
+++ b/greenwave/config.py
@@ -18,6 +18,14 @@ class Config(object):
     # By default, don't cache anything.
     CACHE = {'backend': 'dogpile.cache.null'}
 
+    # These are keys used to construct announcements about decision changes.
+    ANNOUNCEMENT_SUBJECT_KEYS = [
+        ('item', 'type',),
+        ('original_spec_nvr',),
+        ('productmd.compose.id',),
+    ]
+
+
 
 class ProductionConfig(Config):
     DEBUG = False
diff --git a/greenwave/consumers/resultsdb.py b/greenwave/consumers/resultsdb.py
index 625e543..217b73b 100644
--- a/greenwave/consumers/resultsdb.py
+++ b/greenwave/consumers/resultsdb.py
@@ -10,7 +10,6 @@ to the message bus about the newly satisfied/unsatisfied policy.
 """
 
 import collections
-import copy
 import json
 import logging
 
@@ -63,6 +62,22 @@ class ResultsDBHandler(fedmsg.consumers.FedmsgConsumer):
 
         log.info('Greenwave resultsdb handler listening on: %s', self.topic)
 
+    def announcement_subjects(self, config, message):
+        """ Yields subjects for announcement consideration from the message.
+
+        Args:
+            config (dict): The greenwave configuration.
+            message (munch.Munch): A fedmsg about a new result.
+        """
+
+        task = message['msg']['task']
+        announcement_keys = [
+            set(keys) for keys in config['ANNOUNCEMENT_SUBJECT_KEYS']
+        ]
+        for keys in announcement_keys:
+            if keys.issubset(task.keys()):
+                yield dict([(key.decode('utf-8'), task[key].decode('utf-8')) for key in keys])
+
     def consume(self, message):
         """
         Process the given message and take action.
@@ -72,21 +87,24 @@ class ResultsDBHandler(fedmsg.consumers.FedmsgConsumer):
         """
         message = message.get('body', message)
         log.debug('Processing message "%s"', message)
-        self._invalidate_cache(message)
-        self._publish_decision_changes(message)
-
-    def _publish_decision_changes(self, message):
+        config = load_config()
+        testcase = message['msg']['task']['name']
+        result_id = message['msg']['result']['id']
+        for subject in self.announcement_subjects(config, message):
+            log.debug('Considering subject "%s"', subject)
+            self._invalidate_cache(subject)
+            self._publish_decision_changes(config, subject, result_id, testcase)
+
+    def _publish_decision_changes(self, config, subject, result_id, testcase):
         """
-        Process the given message and publish a message if the decision is changed.
+        Process the given subject and publish a message if the decision is changed.
 
         Args:
-            message (munch.Munch): A fedmsg about a new result.
+            config (dict): The greenwave configuration.
+            subject (munch.Munch): A subject argument, used to query greenwave.
+            result_id (int): A result ID to ignore for comparison.
+            testcase (munch.Munch): The name of a testcase to consider.
         """
-        msg = copy.deepcopy(message['msg'])
-        task = msg['task']
-        testcase = task['name']
-        del task['name']
-        config = load_config()
 
         # Build a set of all policies which might apply to this new results
         applicable_policies = set()
@@ -109,7 +127,7 @@ class ResultsDBHandler(fedmsg.consumers.FedmsgConsumer):
                 data = {
                     'decision_context': decision_context,
                     'product_version': product_version,
-                    'subject': [task],
+                    'subject': [subject],
                 }
                 response = requests_session.post(
                     self.fedmsg_config['greenwave_api_url'] + '/decision',
@@ -119,7 +137,7 @@ class ResultsDBHandler(fedmsg.consumers.FedmsgConsumer):
                 decision = response.json()
                 # get old decision
                 data.update({
-                    'ignore_result': [msg['result']['id']],
+                    'ignore_result': [result_id],
                 })
                 response = requests_session.post(
                     self.fedmsg_config['greenwave_api_url'] + '/decision',
@@ -129,7 +147,7 @@ class ResultsDBHandler(fedmsg.consumers.FedmsgConsumer):
                 old_decision = response.json()
                 if decision != old_decision:
                     decision.update({
-                        'subject': [task],
+                        'subject': [subject],
                         'decision_context': decision_context,
                         'product_version': product_version,
                         'previous': old_decision,
@@ -138,20 +156,16 @@ class ResultsDBHandler(fedmsg.consumers.FedmsgConsumer):
                               'greenwave.decision.update')
                     fedmsg.publish(topic='decision.update', msg=decision)
 
-    def _invalidate_cache(self, message):
+    def _invalidate_cache(self, subject):
         """
-        Process the given message and delete cache keys as necessary.
+        Process the given subject and delete cache keys as necessary.
 
         Args:
-            message (munch.Munch): A fedmsg about a new result or waiver.
+            subject (munch.Munch): A subject argument, used to query greenwave.
         """
-        msg = copy.deepcopy(message['msg'])
-        task = msg['task']
-        del task['name']
-        # here, task is {"item": "nodejs-ansi-black-0.1.1-1.fc28", "type": "koji_build" }
         namespace = None
         fn = greenwave.resources.retrieve_results
-        key = greenwave.cache.key_generator(namespace, fn)(task)
+        key = greenwave.cache.key_generator(namespace, fn)(subject)
         if not self.cache.get(key):
             log.debug("No cache value found for %r", key)
         else:

