From 464aef293ca84eb12749cd34da661e14ad774d18 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Barth Date: Sun, 4 Oct 2015 23:37:50 +0200 Subject: [PATCH] Add SWF endpoint GetWorkflowExecutionHistory and associated HistoryEvent model --- moto/swf/models/__init__.py | 2 + moto/swf/models/history_event.py | 59 ++++++++++++++++++++++ moto/swf/models/workflow_execution.py | 21 ++++++++ moto/swf/responses.py | 12 +++++ tests/test_swf/__init__.py | 0 tests/test_swf/test_models.py | 43 ++++++++++++---- tests/test_swf/test_workflow_executions.py | 23 +++++++++ tests/test_swf/utils.py | 24 +++++++++ 8 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 moto/swf/models/history_event.py create mode 100644 tests/test_swf/__init__.py create mode 100644 tests/test_swf/utils.py diff --git a/moto/swf/models/__init__.py b/moto/swf/models/__init__.py index a7be6f64e..c47d704a2 100644 --- a/moto/swf/models/__init__.py +++ b/moto/swf/models/__init__.py @@ -15,6 +15,7 @@ from ..exceptions import ( from .activity_type import ActivityType from .domain import Domain from .generic_type import GenericType +from .history_event import HistoryEvent from .workflow_type import WorkflowType from .workflow_execution import WorkflowExecution @@ -150,6 +151,7 @@ class SWFBackend(BaseBackend): wfe = WorkflowExecution(wf_type, workflow_id, tag_list=tag_list, **kwargs) domain.add_workflow_execution(wfe) + wfe.start() return wfe diff --git a/moto/swf/models/history_event.py b/moto/swf/models/history_event.py new file mode 100644 index 000000000..c88f9fbe9 --- /dev/null +++ b/moto/swf/models/history_event.py @@ -0,0 +1,59 @@ +from __future__ import unicode_literals +from datetime import datetime +from time import mktime + + +class HistoryEvent(object): + def __init__(self, event_id, event_type, **kwargs): + self.event_id = event_id + self.event_type = event_type + self.event_timestamp = float(mktime(datetime.now().timetuple())) + for key, value in kwargs.iteritems(): + self.__setattr__(key, value) + # break soon if attributes are not valid + self.event_attributes() + + def to_dict(self): + return { + "eventId": self.event_id, + "eventType": self.event_type, + "eventTimestamp": self.event_timestamp, + self._attributes_key(): self.event_attributes() + } + + def _attributes_key(self): + key = "{}EventAttributes".format(self.event_type) + key = key[0].lower() + key[1:] + return key + + def event_attributes(self): + if self.event_type == "WorkflowExecutionStarted": + wfe = self.workflow_execution + hsh = { + "childPolicy": wfe.child_policy, + "executionStartToCloseTimeout": wfe.execution_start_to_close_timeout, + "parentInitiatedEventId": 0, + "taskList": { + "name": wfe.task_list + }, + "taskStartToCloseTimeout": wfe.task_start_to_close_timeout, + "workflowType": { + "name": wfe.workflow_type.name, + "version": wfe.workflow_type.version + } + } + return hsh + elif self.event_type == "DecisionTaskScheduled": + wfe = self.workflow_execution + return { + "startToCloseTimeout": wfe.task_start_to_close_timeout, + "taskList": {"name": wfe.task_list} + } + elif self.event_type == "DecisionTaskStarted": + return { + "scheduledEventId": self.scheduled_event_id + } + else: + raise NotImplementedError( + "HistoryEvent does not implement attributes for type '{}'".format(self.event_type) + ) diff --git a/moto/swf/models/workflow_execution.py b/moto/swf/models/workflow_execution.py index 345809748..fa6d28dd0 100644 --- a/moto/swf/models/workflow_execution.py +++ b/moto/swf/models/workflow_execution.py @@ -4,6 +4,7 @@ import uuid from moto.core.utils import camelcase_to_underscores from ..exceptions import SWFDefaultUndefinedFault +from .history_event import HistoryEvent class WorkflowExecution(object): @@ -29,6 +30,8 @@ class WorkflowExecution(object): "openActivityTasks": 0, "openChildWorkflowExecutions": 0, } + # events + self.events = [] def __repr__(self): return "WorkflowExecution(run_id: {})".format(self.run_id) @@ -88,3 +91,21 @@ class WorkflowExecution(object): #counters hsh["openCounts"] = self.open_counts return hsh + + def next_event_id(self): + event_ids = [evt.event_id for evt in self.events] + return max(event_ids or [0]) + + def _add_event(self, *args, **kwargs): + evt = HistoryEvent(self.next_event_id(), *args, **kwargs) + self.events.append(evt) + + def start(self): + self._add_event( + "WorkflowExecutionStarted", + workflow_execution=self, + ) + self._add_event( + "DecisionTaskScheduled", + workflow_execution=self, + ) diff --git a/moto/swf/responses.py b/moto/swf/responses.py index 210a5be15..8f7aa0344 100644 --- a/moto/swf/responses.py +++ b/moto/swf/responses.py @@ -209,3 +209,15 @@ class SWFResponse(BaseResponse): wfe = self.swf_backend.describe_workflow_execution(domain_name, run_id, workflow_id) return json.dumps(wfe.to_full_dict()) + + def get_workflow_execution_history(self): + domain_name = self._params["domain"] + _workflow_execution = self._params["execution"] + run_id = _workflow_execution["runId"] + workflow_id = _workflow_execution["workflowId"] + # TODO: implement reverseOrder + + wfe = self.swf_backend.describe_workflow_execution(domain_name, run_id, workflow_id) + return json.dumps({ + "events": [evt.to_dict() for evt in wfe.events] + }) diff --git a/tests/test_swf/__init__.py b/tests/test_swf/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_swf/test_models.py b/tests/test_swf/test_models.py index 0d54cb67f..a8b76330f 100644 --- a/tests/test_swf/test_models.py +++ b/tests/test_swf/test_models.py @@ -1,9 +1,11 @@ from sure import expect from nose.tools import assert_raises +from freezegun import freeze_time from moto.swf.models import ( Domain, GenericType, + HistoryEvent, WorkflowType, WorkflowExecution, ) @@ -11,15 +13,8 @@ from moto.swf.exceptions import ( SWFDefaultUndefinedFault, ) +from .utils import get_basic_workflow_type -# utils -def test_workflow_type(): - return WorkflowType( - "test-workflow", "v1.0", - task_list="queue", default_child_policy="ABANDON", - default_execution_start_to_close_timeout="300", - default_task_start_to_close_timeout="300", - ) # Domain def test_domain_short_dict_representation(): @@ -91,7 +86,7 @@ def test_type_string_representation(): # WorkflowExecution def test_workflow_execution_creation(): - wft = test_workflow_type() + wft = get_basic_workflow_type() wfe = WorkflowExecution(wft, "ab1234", child_policy="TERMINATE") wfe.workflow_type.should.equal(wft) wfe.child_policy.should.equal("TERMINATE") @@ -130,12 +125,12 @@ def test_workflow_execution_creation_child_policy_logic(): def test_workflow_execution_string_representation(): - wft = test_workflow_type() + wft = get_basic_workflow_type() wfe = WorkflowExecution(wft, "ab1234", child_policy="TERMINATE") str(wfe).should.match(r"^WorkflowExecution\(run_id: .*\)") def test_workflow_execution_generates_a_random_run_id(): - wft = test_workflow_type() + wft = get_basic_workflow_type() wfe1 = WorkflowExecution(wft, "ab1234", child_policy="TERMINATE") wfe2 = WorkflowExecution(wft, "ab1235", child_policy="TERMINATE") wfe1.run_id.should_not.equal(wfe2.run_id) @@ -194,3 +189,29 @@ def test_workflow_execution_full_dict_representation(): "taskList": {"name": "queue"}, "taskStartToCloseTimeout": "300", }) + + +# HistoryEvent +@freeze_time("2015-01-01 12:00:00") +def test_history_event_creation(): + he = HistoryEvent(123, "DecisionTaskStarted", scheduled_event_id=2) + he.event_id.should.equal(123) + he.event_type.should.equal("DecisionTaskStarted") + he.event_timestamp.should.equal(1420110000.0) + +@freeze_time("2015-01-01 12:00:00") +def test_history_event_to_dict_representation(): + he = HistoryEvent(123, "DecisionTaskStarted", scheduled_event_id=2) + he.to_dict().should.equal({ + "eventId": 123, + "eventType": "DecisionTaskStarted", + "eventTimestamp": 1420110000.0, + "decisionTaskStartedEventAttributes": { + "scheduledEventId": 2 + } + }) + +def test_history_event_breaks_on_initialization_if_not_implemented(): + HistoryEvent.when.called_with( + 123, "UnknownHistoryEvent" + ).should.throw(NotImplementedError) diff --git a/tests/test_swf/test_workflow_executions.py b/tests/test_swf/test_workflow_executions.py index e73d37f23..27feb8b4a 100644 --- a/tests/test_swf/test_workflow_executions.py +++ b/tests/test_swf/test_workflow_executions.py @@ -90,3 +90,26 @@ def test_describe_non_existent_workflow_execution(): "__type": "com.amazonaws.swf.base.model#UnknownResourceFault", "message": "Unknown execution: WorkflowExecution=[workflowId=wrong-workflow-id, runId=wrong-run-id]" }) + + +# GetWorkflowExecutionHistory endpoint +@mock_swf +def test_get_workflow_execution_history(): + conn = setup_swf_environment() + hsh = conn.start_workflow_execution("test-domain", "uid-abcd1234", "test-workflow", "v1.0") + run_id = hsh["runId"] + + resp = conn.get_workflow_execution_history("test-domain", run_id, "uid-abcd1234") + resp["events"].should.be.a("list") + evt = resp["events"][0] + evt["eventType"].should.equal("WorkflowExecutionStarted") + + +@mock_swf +def test_get_workflow_execution_history_on_non_existent_workflow_execution(): + conn = setup_swf_environment() + + with assert_raises(SWFUnknownResourceFault) as err: + conn.get_workflow_execution_history("test-domain", "wrong-run-id", "wrong-workflow-id") + + # (the rest is already tested above) diff --git a/tests/test_swf/utils.py b/tests/test_swf/utils.py new file mode 100644 index 000000000..f106e2e02 --- /dev/null +++ b/tests/test_swf/utils.py @@ -0,0 +1,24 @@ +from moto.swf.models import ( + WorkflowType, +) + + +# A generic test WorkflowType +def _generic_workflow_type_attributes(): + return [ + "test-workflow", "v1.0" + ], { + "task_list": "queue", + "default_child_policy": "ABANDON", + "default_execution_start_to_close_timeout": "300", + "default_task_start_to_close_timeout": "300", + } + +def get_basic_workflow_type(): + args, kwargs = _generic_workflow_type_attributes() + return WorkflowType(*args, **kwargs) + +def mock_basic_workflow_type(domain_name, conn): + args, kwargs = _generic_workflow_type_attributes() + conn.register_workflow_type(domain_name, *args, **kwargs) + return conn