Zeta Workflow 09: Execution State Registry
Since we potentially have multiple executions, we need a registry to manage the multiple Execution State objects. The registry also maintains the notion of which is the “current” Execution State object. The features are:
- Accept notification that “this” is the current execution.
- For a given execution, return the relevant Execution State object. We either fetch it from our cache, or construct it on the spot. I am assuming that, by this point, each execution has an id. If not, initially, throw an exception. Later, we should probably ignore executions with no id, assuming that later in the workflow execution it WILL have an id, and we can snag it at that point.
- Return the “current” Execution State object.
Test Outcomes
For the following tests, $execution is a Zeta Workflow execution object and $executionState is an Execution State object. We consider uniqueness according to $execution->getId() and the corresponding $executionState->getId().
- getExecutionState($execution) returns an initialized Execution State object. The object is initialized via captureExecution().
- A second call to getExecutionState() with the same $execution returns the same initialized Execution State object.
- A second call to getExecutionState() with a different $execution returns a different initialized Execution State object.
- Successive calls to getExecutionState() each call captureExecution on the current execution object.
- If $execution->getId() returns null, we throw an exception.
- setCurrentExecution($execution) marks the current execution as the current one, and calls getExecutionState() to update the current execution state. The method returns the getExecutionState() result. A later call to getCurrentExecutionState() returns the same Execution State object regardless of intervening getExecutionState() calls.
For the test fixture, we’ll use mock execution objects and real Execution State objects. We’ll begin with a brute force setup of each test case, and refactor/simplify as we see the opportunity.
Here comes the first test.
namespace StarTribune\Workflow\Tests\Sniffer; use Mockery as m; class executionStateRegistryTest extends \TestCase { public $fixture; public $execution; public $state; public $expectedVariables = array('one' => 1, 'two' => 2); public $expectedId = 1234; public function setUp() { parent::setUp(); $this->fixture = new \StarTribune\Workflow\Sniffer\ExecutionStateRegistry; $this->execution = m::mock('\ezcWorkflowExecution'); } public function conditionExecution() { $this->execution->shouldReceive('isCancelled') ->once() ->andReturn('cancel'); $this->execution->shouldReceive('hasEnded') ->once() ->andReturn('ended'); $this->execution->shouldReceive('isSuspended') ->once() ->andReturn('suspended'); $this->execution->shouldReceive('getVariables') ->once() ->andReturn($this->expectedVariables); $this->execution->shouldReceive('getId') ->once() ->andReturn($this->expectedId); } public function testFixtureCorrectClass() { $this->assertTrue($this->fixture instanceof \StarTribune\Workflow\Sniffer\ExecutionStateRegistry); } public function testMockCorrectClass() { $this->assertTrue($this->execution instanceof \ezcWorkflowExecution); } public function testGetExecutionStateReturnsExecutionState() { $this->conditionExecution(); $actual = $this->fixture->getExecutionState($this->execution); $this->assertTrue($actual instanceof \StarTribune\Workflow\Sniffer\ExecutionState); } }
The production code.
namespace StarTribune\Workflow\Sniffer; use ExecutionState; class ExecutionStateRegistry { public function getExecutionState(\ezcWorkflowExecution $execution) { $state = new \StarTribune\Workflow\Sniffer\ExecutionState; $state->captureExecution($execution); return $state; } }
A second call to getExecutionState() with the same $execution returns the same initialized Execution State object.
public function testSecondGetExecutionStateReturnsSame() { $this->conditionExecution(); $first = $this->fixture->getExecutionState($this->execution); $second = $this->fixture->getExecutionState($this->execution); $this->assertSame($first, $second); $this->assertEquals($first->getId(), $second->getId()); }
The production code.
class ExecutionStateRegistryException extends \Exception { } class ExecutionStateRegistry { private $_registry; public function __construct() { $this->_registry = array(); } public function getExecutionState(\ezcWorkflowExecution $execution) { $state = new \StarTribune\Workflow\Sniffer\ExecutionState; $state->captureExecution($execution); $id = $state->getId(); $object = $this->getRegisteredObject($id); if($object) { $object->captureExecution($execution); return $object; } $this->registerObject($id, $state); return $state; } private function getRegisteredObject($id) { return array_key_exists($id, $this->_registry) ? $this->_registry[$id] : null; } private function registerObject($id, \StarTribune\Workflow\Sniffer\ExecutionState $object) { if(array_key_exists($id, $this->_registry)) { throw new ExecutionStateRegistryException("Duplicate registry $id"); } $this->_registry[$id] = $object; } }
A second call to getExecutionState() with a different $execution returns a different initialized Execution State object. Here is the test; no production changes needed.
public function testDifferentExecutionReturnsDifferent() { $this->conditionExecution(); $execution1 = $this->execution; ++$this->expectedId; $this->execution = m::mock('\ezcWorkflowExecution'); $this->conditionExecution(); $execution2 = $this->execution; $this->assertNotEquals($execution1->getId(), $execution2->getId(), "guard clause"); $first = $this->fixture->getExecutionState($execution1); $second = $this->fixture->getExecutionState($execution2); $this->assertNotSame($first, $second); $this->assertNotEquals($first->getId(), $second->getId()); }
Successive calls to getExecutionState() each call captureExecution on the current execution object: We already have that covered.
If $execution->getId() returns null, we throw an exception. The test:
/** * @expectedException \StarTribune\Workflow\Sniffer\ExecutionStateRegistryException */ public function testNullIdException() { $this->expectedId = null; $this->conditionExecution(); $actual = $this->fixture->getExecutionState($this->execution); $this->fail("expected exception"); }
Production code:
public function getExecutionState(\ezcWorkflowExecution $execution) { $state = new \StarTribune\Workflow\Sniffer\ExecutionState; $state->captureExecution($execution); $id = $state->getId(); if(null === $id) { throw new ExecutionStateRegistryException("Null execution id"); } $object = $this->getRegisteredObject($id); if($object) { $object->captureExecution($execution); return $object; } $this->registerObject($id, $state); return $state; }
setCurrentExecution($execution) marks the current execution as the current one, and calls getExecutionState() to update the current execution state. The method returns the getExecutionState() result. A later call to getCurrentExecutionState() returns the same Execution State object regardless of intervening getExecutionState() calls.
We’ll use the “different execution returns different” set up and run the whole thing as a single scenario.
public function testSetCurrentExecutionScenario() { $this->conditionExecution(); $execution1 = $this->execution; ++$this->expectedId; $this->execution = m::mock('\ezcWorkflowExecution'); $this->conditionExecution(); $execution2 = $this->execution; $this->assertNotEquals($execution1->getId(), $execution2->getId(), "guard clause"); $state1 = $this->fixture->setCurrentExecution($execution1); $state2 = $this->fixture->getExecutionState($execution2); $state3 = $this->fixture->getCurrentExecutionState(); $this->assertSame($state1, $state3); $this->assertNotSame($state1, $state2); }
Here is the production code.
private $_currentId; public function setCurrentExecution(\ezcWorkflowExecution $execution) { $state = $this->getExecutionState($execution); $this->_currentId = $state->getId(); return $state; } public function getCurrentExecutionState() { return $this->getRegisteredObject($this->_currentId); }
Once again, we have a small, tight, single-purpose class.
Next up: The Execution State Interpreter.