From 3392e392a353074aa89556ab345e62374470248c Mon Sep 17 00:00:00 2001
From: Nathan Hertz <nhertz@nrao.edu>
Date: Fri, 12 Nov 2021 11:52:03 -0500
Subject: [PATCH] WS-290: Fix QA

---
 ..._change_sendmessage_qa_ready_action_to_.py | 51 +++++++++++++++++
 .../test/test_capability_actions.py           | 20 ++++++-
 .../workspaces/capability/schema.py           | 57 ++++++++++++-------
 .../workflow/services/workflow_service.py     |  6 +-
 4 files changed, 108 insertions(+), 26 deletions(-)
 create mode 100644 schema/versions/ced8e001d262_change_sendmessage_qa_ready_action_to_.py

diff --git a/schema/versions/ced8e001d262_change_sendmessage_qa_ready_action_to_.py b/schema/versions/ced8e001d262_change_sendmessage_qa_ready_action_to_.py
new file mode 100644
index 000000000..ee2b9787c
--- /dev/null
+++ b/schema/versions/ced8e001d262_change_sendmessage_qa_ready_action_to_.py
@@ -0,0 +1,51 @@
+"""change SendMessage qa_ready action to AnnounceQa action
+
+Revision ID: ced8e001d262
+Revises: acfdeb6777cb
+Create Date: 2021-11-10 10:16:09.070171
+
+"""
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "ced8e001d262"
+down_revision = "acfdeb6777cb"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    """
+    Modify action that gets triggered on QA workflow complete;
+    Change it to perform the AnnounceQa action instead of attempting to SendMessage with arguments qa_ready
+    """
+    op.execute(
+        """
+        UPDATE capability_state_actions SET action_type = 'AnnounceQa', arguments = null
+            WHERE action_type = 'SendMessage'
+                AND arguments = 'qa_ready'
+                AND transition_id = (
+                    SELECT transition_id FROM capability_state_transitions
+                    WHERE pattern = 'type == workflow-complete'
+                        AND capability_name = 'std_calibration'
+                )
+        """
+    )
+
+
+def downgrade():
+    """
+    Undo above change
+    """
+    op.execute(
+        """
+        UPDATE capability_state_actions SET action_type = 'SendMessage', arguments = 'qa_ready'
+            WHERE action_type = 'AnnounceQa'
+                AND arguments IS null
+                AND transition_id = (
+                    SELECT transition_id FROM capability_state_transitions
+                    WHERE pattern = 'type == workflow-complete'
+                        AND capability_name = 'std_calibration'
+                )
+        """
+    )
diff --git a/shared/workspaces/test/test_capability_actions.py b/shared/workspaces/test/test_capability_actions.py
index 7ab60b86f..81a209a0b 100644
--- a/shared/workspaces/test/test_capability_actions.py
+++ b/shared/workspaces/test/test_capability_actions.py
@@ -20,6 +20,7 @@
 Testing capability actions
 """
 from typing import Dict, List, Optional
+from unittest.mock import patch
 
 from hypothesis import given
 from hypothesis import strategies as st
@@ -30,6 +31,7 @@ from workspaces.capability.enums import (
     QueueState,
 )
 from workspaces.capability.schema import (
+    AnnounceQa,
     Capability,
     CapabilityExecution,
     CapabilityRequest,
@@ -141,7 +143,7 @@ st.register_type_strategy(
             "queue_state": st.sampled_from([name for name, _ in QueueState.__members__.items()]),
             "capability_request_id": st.integers(min_value=SQLITE_MIN_INT, max_value=SQLITE_MAX_INT),
             "version_number": st.integers(min_value=SQLITE_MIN_INT, max_value=SQLITE_MAX_INT),
-            "current_workflow_request_id": st.just(-11),
+            "current_workflow_request_id": st.integers(min_value=1, max_value=15),
             "delivery_url": st.from_regex(r"https:\/\/dl-nrao-unittest\.aoc\.nrao\.edu(?:\/[A-z0-9]*)+"),
             "created_at": st.datetimes().map(
                 lambda time: time.isoformat(),
@@ -159,7 +161,7 @@ st.register_type_strategy(
     st.from_type(Capability),
     st.integers(min_value=1, max_value=10),
 )
-def test_qapass(
+def test_qa_pass(
     mock_capability_info: CapabilityInfo,
     dummy_execution_manager: ExecutionManager,
     parameters: Optional[List[str]],
@@ -194,3 +196,17 @@ def test_qapass(
                 assert actual_version.state == CapabilityVersionState.Complete.name
             else:
                 assert actual_version.state == CapabilityVersionState.Failed.name
+
+
+@given(st.from_type(CapabilityExecution))
+def test_announce_qa(execution_json: Dict):
+    execution = CapabilityExecution.from_json(execution_json)
+    fake_workflow_url = "http://fake-workflow.edu"
+
+    with patch("workspaces.capability.schema.requests.post") as mock_post:
+        with patch("workspaces.capability.schema.CapoConfig") as mock_capo_config:
+            mock_capo_config.return_value.settings.return_value.serviceUrl = fake_workflow_url
+            AnnounceQa()(execution)
+            mock_post.assert_called_with(
+                f"{fake_workflow_url}/workflows/requests/{execution.current_workflow_request_id}/qa/qa_ready"
+            )
diff --git a/shared/workspaces/workspaces/capability/schema.py b/shared/workspaces/workspaces/capability/schema.py
index 749c4f078..106b6ac53 100644
--- a/shared/workspaces/workspaces/capability/schema.py
+++ b/shared/workspaces/workspaces/capability/schema.py
@@ -28,7 +28,9 @@ import re
 from typing import Dict, List
 
 import pendulum
+import requests
 import sqlalchemy as sa
+from pycapo import CapoConfig
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy.orm import relationship
 
@@ -87,9 +89,7 @@ class Action(Base, ActionIF):
 
 
 class Noop(Action):
-    __mapper_args__ = {
-        "polymorphic_identity": "Noop",
-    }
+    __mapper_args__ = {"polymorphic_identity": "Noop"}
 
     def __call__(self, execution: CapabilityExecutionIF, manager: ExecutionManagerIF):
         # empty because this is the no-operation action
@@ -97,9 +97,7 @@ class Noop(Action):
 
 
 class ExecuteWorkflow(Action):
-    __mapper_args__ = {
-        "polymorphic_identity": "ExecuteWorkflow",
-    }
+    __mapper_args__ = {"polymorphic_identity": "ExecuteWorkflow"}
 
     def __call__(self, execution: CapabilityExecutionIF, manager: ExecutionManagerIF):
         logger.info("Executing workflow %s for execution %s", self.arguments, execution.id)
@@ -121,9 +119,7 @@ class ExecuteWorkflow(Action):
 
 
 class QueueWorkflow(Action):
-    __mapper_args__ = {
-        "polymorphic_identity": "QueueWorkflow",
-    }
+    __mapper_args__ = {"polymorphic_identity": "QueueWorkflow"}
 
     def __call__(self, execution: CapabilityExecutionIF, manager: ExecutionManagerIF):
         # message the execution manager
@@ -132,9 +128,7 @@ class QueueWorkflow(Action):
 
 
 class SendNotification(Action):
-    __mapper_args__ = {
-        "polymorphic_identity": "SendNotification",
-    }
+    __mapper_args__ = {"polymorphic_identity": "SendNotification"}
 
     def __call__(self, execution: CapabilityExecutionIF, manager: ExecutionManagerIF):
         # requests.post(f"/notifications/{self.notification}/send", users=self.user)
@@ -147,9 +141,7 @@ class SendMessage(Action):
     Action that simply sends an AMQP message with the type of the given arguments
     """
 
-    __mapper_args__ = {
-        "polymorphic_identity": "SendMessage",
-    }
+    __mapper_args__ = {"polymorphic_identity": "SendMessage"}
 
     def __call__(self, execution: CapabilityExecutionIF, manager: ExecutionManagerIF):
         message_type = self.arguments
@@ -164,15 +156,14 @@ class QaPass(Action):
     Action that performs system-level operations required by QA-passing a capability request
     """
 
-    __mapper_args__ = {
-        "polymorphic_identity": "QaPass",
-    }
+    __mapper_args__ = {"polymorphic_identity": "QaPass"}
 
     def _fail_other_versions(self, request: CapabilityRequest):
         """
-        Given a capability request, set the executions of all of its versions besides the current version to Failed
+        Given a capability request, set the state of the current version to Complete, and the state of all other
+        versions to Failed
 
-        :param request: capability request
+        :param request: Capability request
         """
         versions = request.versions
         for version in versions:
@@ -183,12 +174,36 @@ class QaPass(Action):
                 # Mark all other versions as failed
                 version.state = CapabilityVersionState.Failed.name
 
-    def __call__(self, execution: CapabilityExecutionIF, manager: ExecutionManagerIF):
+    def _abort_running_executions(self, request: CapabilityRequest):
+        """
+        Given a capability request, find all running executions associated with it and abort them
+
+        :param request: Capability request
+        """
+        raise NotImplementedError
+
+    def __call__(self, execution: CapabilityExecutionIF, *args):
         logger.info("Execution #%s has passed QA", execution.id)
         request = execution.capability_request
         self._fail_other_versions(request)
 
 
+class AnnounceQa(Action):
+    """
+    Action that makes a REST call to the announce_qa endpoint in the workflow service, which announces that an execution
+    is ready for QA
+    """
+
+    __mapper_args__ = {"polymorphic_identity": "AnnounceQa"}
+
+    def __call__(self, execution: CapabilityExecutionIF, *args):
+        workflow_request_id = execution.current_workflow_request_id
+        msg_type = "qa_ready"
+        workflow_service_url = CapoConfig().settings("edu.nrao.workspaces.WorkflowSettings").serviceUrl
+        url = f"{workflow_service_url}/workflows/requests/{workflow_request_id}/qa/{msg_type}"
+        requests.post(url)
+
+
 class State(Base):
     transitions: List[Transition]
 
diff --git a/shared/workspaces/workspaces/workflow/services/workflow_service.py b/shared/workspaces/workspaces/workflow/services/workflow_service.py
index e07ead320..81dbddbbc 100644
--- a/shared/workspaces/workspaces/workflow/services/workflow_service.py
+++ b/shared/workspaces/workspaces/workflow/services/workflow_service.py
@@ -27,8 +27,6 @@ from pathlib import Path
 from tempfile import mkdtemp
 from typing import Dict, List, Union
 
-# pylint: disable=E0401, W1203
-
 import requests
 import transaction
 from messaging.messenger import MessageSender
@@ -45,6 +43,8 @@ from workspaces.workflow.message_architect import (
 from workspaces.workflow.schema import Workflow, WorkflowRequest, WorkflowRequestFile
 from workspaces.workflow.services.interfaces import WorkflowInfoIF, WorkflowServiceIF
 
+# pylint: disable=E0401, W1203
+
 logger = logging.getLogger(__name__)
 
 
@@ -574,7 +574,7 @@ class WorkflowService(WorkflowServiceIF):
         wf_request = self.info.lookup_workflow_request(workflow_request_id)
 
         qa_event_msg = WorkflowMessageArchitect(request=wf_request).compose_message(msg_type)
-        self.archive_messenger.send_message(**qa_event_msg)
+        self.messenger.send_message(**qa_event_msg)
 
         return qa_event_msg
 
-- 
GitLab