Zeta Workflow 10: Execution State Interpreter

The inter­preter returns a text string. Here is the logic:

  1. If the ‘can­cel’ vari­able exists, return “cancel”.
  2. If the ‘swap’ vari­able exists, it names the new work­flow. Return “swap “ fol­lowed by the new work­flow name. See the below discussion.
  3. If the ‘slide’ vari­able exists, return “slide “ fol­lowed by the slide variable’s value.
  4. Oth­er­wise, return an empty string.

The above steps are in priority order.

The class has only a single method, interpretExecutionState(ExecutionState $state). The object does not need to cache anything; it simply interprets the passed-in execution state object, returning a string as the interpretation.

Given that our unit tests already interpret the slide variable, we can adapt one of those tests to exercise the interpreter. Once interpreter development is complete (with unit tests), make a copy of the subWorkflowTest.php. Change the copy’s verifySlide method to use the interpreter. All sub workflow tests should continue to pass while using the Execution State Interpreter.

Oops, I got ahead of myself. We’ll adapt a copy of the subWorkflowTest once the entire sniffer is complete and is getting attached to the workflow.

Test Outcomes

Given the above simplistic interpretation rules, we have eight possible outcomes depending on whether the cancel, swap, slide variables do or do not exist. We can set up a single test with a data provider driving the eight scenarios.

In the data provider, we’ll use null to indicate the variable does not exist, and its value otherwise. We can condition a mock ExecutionState object.

After the several iterations fleshing out the tests and production code, here is the result.

The test:

namespace StarTribune\Workflow\Tests\Sniffer;
use Mockery as m;
class executionStateInterpreterTest extends \TestCase {
    public $state;
    public $fixture;

    public function setUp() {
        parent::setUp();
        $this->state = m::mock('\StarTribune\Workflow\Sniffer\ExecutionState');
        $this->fixture = new \StarTribune\Workflow\Sniffer\ExecutionStateInterpreter;
    }

    public function conditionState($cancel, $swap, $slide) {
        $this->conditionStateVariable('cancel', $cancel);
        $this->conditionStateVariable('swap', $swap);
        $this->conditionStateVariable('slide', $slide);
    }

    public function conditionStateVariable($key, $value) {
        if(null === $value) {
            $this->state->shouldReceive('hasVariable')
                ->with($key)
                ->andReturn(false);
        } else {
            $this->state->shouldReceive('hasVariable')
                ->with($key)
                ->andReturn(true);
            $this->state->shouldReceive('getVariable')
                ->with($key)
                ->andReturn($value);
        }
    }

    public function testMockCorrectClass() {
        $this->assertTrue($this->state instanceof \StarTribune\Workflow\Sniffer\ExecutionState);
    }

    public function testFixtureCorrectClass() {
        $this->assertTrue($this->fixture instanceof \StarTribune\Workflow\Sniffer\ExecutionStateInterpreter);
    }

    /**
     * @dataProvider dataInterpretation
     */
    public function testInterpretation($cancel, $swap, $slide, $result) {
        $this->conditionState($cancel, $swap, $slide);
        $actual = $this->fixture->interpretExecutionState($this->state);
        $this->assertSame($result, $actual);
    }

    public function dataInterpretation() {
        $data = array();

        $data[] = array(null, null, null, '');

        $data[] = array(1,        null, null, 'cancel');
        $data[] = array(0,        null, null, 'cancel');
        $data[] = array('',       null, null, 'cancel');
        $data[] = array('cancel', null, null, 'cancel');
        $data[] = array(1,        1,    null, 'cancel');
        $data[] = array(1,        null, 1,    'cancel');
        $data[] = array(1,        1,    1,    'cancel');

        $data[] = array(null, 1,      null, 'swap 1');
        $data[] = array(null, 0,      null, 'swap 0');
        $data[] = array(null, '',     null, 'swap ');
        $data[] = array(null, 'swap', null, 'swap swap');
        $data[] = array(null, 1,      1234, 'swap 1');
        $data[] = array(null, 0,      1234, 'swap 0');
        $data[] = array(null, '',     1234, 'swap ');
        $data[] = array(null, 'swap', 1234, 'swap swap');

        $data[] = array(null, null, 1,       'slide 1');
        $data[] = array(null, null, 0,       'slide 0');
        $data[] = array(null, null, '',      'slide ');
        $data[] = array(null, null, 'slide', 'slide slide');

        return $data;
    }

}

The production class:

namespace StarTribune\Workflow\Sniffer;
use \StarTribune\Workflow\Sniffer\ExecutionState;

class ExecutionStateInterpreter {
    public function interpretExecutionState(\StarTribune\Workflow\Sniffer\ExecutionState $state) {
        $result = '';

        if($state->hasVariable('cancel')) {
            $result = 'cancel';
        } elseif($state->hasVariable('swap')) {
            $result = 'swap '.$state->getVariable('swap');
        } elseif($state->hasVariable('slide')) {
            $result = 'slide '.$state->getVariable('slide');
        }

        return $result;
    }
}

Next up: The Sniffer Plugin

Be the first to comment - What do you think?  Posted by admin - March 29, 2014 at 9:45 pm

Categories: Zeta Components Workflow   Tags:

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().

  1. getExecutionState($execution) returns an initialized Execution State object. The object is initialized via captureExecution().
  2. A second call to getExecutionState() with the same $execution returns the same initialized Execution State object.
  3. A second call to getExecutionState() with a different $execution returns a different initialized Execution State object.
  4. Successive calls to getExecutionState() each call captureExecution on the current execution object.
  5. If $execution->getId() returns null, we throw an exception.
  6. 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 sec­ond call to getEx­e­cu­tion­State() with the same $exe­cu­tion returns the same ini­tial­ized Exe­cu­tion 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 sec­ond call to getEx­e­cu­tion­State() with a dif­fer­ent $exe­cu­tion returns a dif­fer­ent ini­tial­ized Exe­cu­tion 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());
    }

Suc­ces­sive calls to getEx­e­cu­tion­State() each call cap­ture­Ex­e­cu­tion on the cur­rent exe­cu­tion 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 cur­rent exe­cu­tion as the cur­rent one, and calls getEx­e­cu­tion­State() to update the cur­rent exe­cu­tion state. The method returns the getEx­e­cu­tion­State() result. A later call to getCur­rentEx­e­cu­tion­State() returns the same Exe­cu­tion State object regard­less of inter­ven­ing getEx­e­cu­tion­State() 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.

Be the first to comment - What do you think?  Posted by admin - at 8:25 pm

Categories: Zeta Components Workflow   Tags:

Zeta Workflow 08: Execution State Object

The execution state object retains the execution state as defined by isCancelled(), hasEnded(), and isSuspended(); and the execution variables as obtained via getVariables().

The functionality is:

  • Capture execution state, given the execution as passed to the plugin.
  • Disgorge the last-captured execution state. Mirror the execution methods isCancelled(), hasEnded(), isSuspended, getVariables(), getVariable(), hasVariable(), getId().

Our unit tests will explore the API of each method, followed by verifying the functionality in each case.

Starting Point

We begin with a list of unit test outcomes. I’m making this list off the top of my head based on the above description. We may add or change tests as we go:

  1. Can create a mock Zeta Workflow Execution object.
  2. The Zeta Workflow Execution object is an instance of the correct class, ezcWorkflowExecution.
  3. Can capture isCancelled().
  4. Can capture hasEnded().
  5. Can capture isSuspended().
  6. Can capture getVariables(). getVariable() and hasVariable are based on getVariables() content, so there is no need for separate capture mechanisms.
  7. Can capture getId().
  8. A new capture completely replaces any previous capture.
  9. captureExecution($execution) captures the above items 3-7.
  10. If any of the retrieval functions is called before the execution was captured, the retrieval function returns null.
  11. The retrieval functions return their respective captured isCancelled(), hasEnded(), isSuspended(), getVariables(), getId().
  12. getVariables(), when there are no variables, returns empty array.
  13. getVariable(), if the variable does not exist in the current capture, returns null.
  14. getVariable(), if the variable does exist in the current capture, returns the variable’s value.
  15. hasVariable() returns true or false for whether or not that variable exists in the capture.

We start coding by:

  1. Create code directory Sniffer and test directory tests/sniffer.
  2. Define a new test suite in the phpunit.xml.
  3. Define the test suite in the Makefile.
  4. Set up this test suite as the default target in the Makefile.
  5. Create a new test sniffer/executionStateTest.php which extends TestCase. We do not need to extend the database test case.
  6. Create a failing test with $this->fail(‘here’). Run the Makefile and observe the test failure.
  7. Proceed with the first of the above test outcomes.

Here is the initial unit test, set to fail:

<?php
/**
 * Execution State Object
 * @package StarTribune/Workflow
 *
 * @author ewb (3/23/2014)
 */
namespace StarTribune\Workflow\Tests\Sniffer;
use Mockery as m;
/**
 * Execution State Object
 * @package StarTribune/Workflow
 */
class executionStateTest extends \TestCase {

    public function testHere() {
        $this->fail("here we are");
    }
}

With Makefile and phpunit.xml correctly set up, we see the failure:

vendor/bin/phpunit  --testsuite single
PHPUnit 4.0.12 by Sebastian Bergmann.

Configuration read from ***/phpunit.xml

F

Time: 66 ms, Memory: 8.00Mb

There was 1 failure:

1) StarTribune\Workflow\Tests\Sniffer\executionStateTest::testHere
here we are

***/workbench/star-tribune/workflow/tests/sniffer/executionStateTest.php:17
                                     
FAILURES!
Tests: 1, Assertions: 0, Failures: 1.
make: *** [single] Error 1

Now that we know we are running the tests we think we are, we can proceed.

Mock Object

We start with a couple of tests ensuring we get our mock objects set up correctly.

In each case, we start by observing the test failing, then watch it pass. With the first test, we comment-out the assert to ensure that the expectation fails for not being met. In the second test, we just let it ride; it’s trivial. But it’s important to have the test because it “nails down” the execution class.

    public function testMockSetup() {
        $this->execution->shouldReceive('myMethod')
            ->once()
            ->andReturn('foo');
        $this->assertEquals('foo', $this->execution->myMethod());
    }

    public function testMockCorrectClass() {
        $this->assertTrue($this->execution instanceof \ezcWorkflowExecution);
    }

The Capture Methods

We write the tests first. In this case we only confirm that the (not yet existent) functions are callable. When we get to the retrieval functions, we’ll flesh out the actual capture code.

The first test requires that we create the actual class.

    public $execution;
    public $fixture;

    public function setUp() {
        $this->execution = m::mock('\ezcWorkflowExecution');
        $this->fixture = new \StarTribune\Workflow\Sniffer\ExecutionState;
    }

    public function testFixtureCorrectClass() {
        $this->assertTrue($this->fixture instanceof \StarTribune\Workflow\Sniffer\ExecutionState);
    }

Once the test passes, we refactor the construction to setUp() as shown above. Here is our class.

<?php
/**
 * Track Zeta Workflow execution state.
 * @package StarTribune/Workflow
 */

namespace StarTribune\Workflow\Sniffer;

/**
 * Track Zeta Workflow execution state.
 *
 * @package StarTribune/Workflow
 *
 * @author ewb (3/29/2014)
 */
class ExecutionState {
}

Now that the class exists, we write just enough code to make our capture tests pass.

The first pair of capture tests looks like this:

    public function testCaptureIsCancelledAPI() {
        $this->execution->shouldReceive('isCancelled')->once();
        $return = $this->fixture->captureIsCancelled($this->execution);
        $this->assertTrue(null === $return, "Method should return null");
    }

    public function testCaptureIsCancelledCalls() {
        $this->execution->shouldReceive('isCancelled')->once();
        $return = $this->fixture->captureIsCancelled($this->execution);
    }

Now that we’ve written it, we can see the first of the two tests is redundant. Any purpose served by the first test is served by the second.

Here is the class as written to pass the above tests.

class ExecutionState {
    public $isCancelled;

    public function captureIsCancelled(\ezcWorkflowExecution $execution) {
        $this->isCancelled = $execution->isCancelled();
    }
}

The rest of the captures and production code will be similar. Here are the tests.

    public function testCaptureIsCancelledCalls() {
        $this->execution->shouldReceive('isCancelled')->once();
        $return = $this->fixture->captureIsCancelled($this->execution);
    }

    public function testCaptureHasEnded() {
        $this->execution->shouldReceive('hasEnded')->once();
        $return = $this->fixture->captureHasEnded($this->execution);
    }

    public function testCaptureIsSuspended() {
        $this->execution->shouldReceive('isSuspended')->once();
        $return = $this->fixture->captureIsSuspended($this->execution);
    }

    public function testCaptureGetVariables() {
        $this->execution->shouldReceive('getVariables')->once();
        $return = $this->fixture->captureGetVariables($this->execution);
    }

    public function testCaptureGetId() {
        $this->execution->shouldReceive('getId')->once();
        $return = $this->fixture->captureGetId($this->execution);
    }

    public function testCaptureExecution() {
        $this->execution->shouldReceive('isCancelled')->once();
        $this->execution->shouldReceive('hasEnded')->once();
        $this->execution->shouldReceive('isSuspended')->once();
        $this->execution->shouldReceive('getVariables')->once();
        $this->execution->shouldReceive('getId')->once();
        $return = $this->fixture->captureExecution($this->execution);
    }

Here is the production code.

class ExecutionState {
    public $isCancelled;
    public $hasEnded;
    public $isSuspended;
    public $variables;
    public $id;

    public function captureIsCancelled(\ezcWorkflowExecution $execution) {
        $this->isCancelled = $execution->isCancelled();
    }

    public function captureHasEnded(\ezcWorkflowExecution $execution) {
        $this->hasEnded = $execution->hasEnded();
    }

    public function captureIsSuspended(\ezcWorkflowExecution $execution) {
        $this->isSuspended = $execution->isSuspended();
    }

    public function captureGetVariables(\ezcWorkflowExecution $execution) {
        $this->variables = $execution->getVariables();
    }

    public function captureGetId(\ezcWorkflowExecution $execution) {
        $this->id = $execution->getId();
    }

    public function captureExecution(\ezcWorkflowExecution $execution) {
        $this->captureIsCancelled($execution);
        $this->captureHasEnded($execution);
        $this->captureIsSuspended($execution);
        $this->captureGetVariables($execution);
        $this->captureGetId($execution);
    }
}

Retrieval Functions

We are skipping the rule “a new capture completely replaces the old capture”. It will be clear from inspecting the production code that this is the case, and I don’t think the test is sufficiently worth the time to write it.

Let’s test that any retrieval prior to the first capture returns null.

    public function testIsCancelledNull() {
        $actual = $this->fixture->isCancelled();
        $this->assertTrue(null === $actual);
    }

    public function testHasEndedNull() {
        $actual = $this->fixture->hasEnded();
        $this->assertTrue(null === $actual);
    }

    public function testIsSuspendedNull() {
        $actual = $this->fixture->isSuspended();
        $this->assertTrue(null === $actual);
    }

    public function testGetVariablesNull() {
        $actual = $this->fixture->getVariables();
        $this->assertTrue(null === $actual);
    }

    public function testGetIdNull() {
        $actual = $this->fixture->getId();
        $this->assertTrue(null === $actual);
    }

I changed the captured properties from public to private. Here is the new production code.

class ExecutionState {
    private $_isCancelled;
    private $_hasEnded;
    private $_isSuspended;
    private $_variables;
    private $_id;

    public function captureIsCancelled(\ezcWorkflowExecution $execution) {
        $this->_isCancelled = $execution->isCancelled();
    }

    public function captureHasEnded(\ezcWorkflowExecution $execution) {
        $this->_hasEnded = $execution->hasEnded();
    }

    public function captureIsSuspended(\ezcWorkflowExecution $execution) {
        $this->_isSuspended = $execution->isSuspended();
    }

    public function captureGetVariables(\ezcWorkflowExecution $execution) {
        $this->_variables = $execution->getVariables();
    }

    public function captureGetId(\ezcWorkflowExecution $execution) {
        $this->_id = $execution->getId();
    }

    public function captureExecution(\ezcWorkflowExecution $execution) {
        $this->captureIsCancelled($execution);
        $this->captureHasEnded($execution);
        $this->captureIsSuspended($execution);
        $this->captureGetVariables($execution);
        $this->captureGetId($execution);
    }

    public function isCancelled() {
        return $this->_isCancelled;
    }

    public function hasEnded() {
        return $this->_hasEnded;
    }

    public function isSuspended() {
        return $this->_isSuspended;
    }

    public function getVariables() {
        return $this->_variables;
    }

    public function getId() {
        return $this->_id;
    }

}

To test that the retrieval really does retrieve the right information, we should go to a different test fixture setup. This means starting a new test class. Here is tests/sniffer/executionStateRetrievalTest.php, with the retrieval functions developed.

class executionStateRetrievalTest extends \TestCase {
    public $execution;
    public $fixture;
    public $expectedVariables = array('one' => 1, 'two' => 2);
    public $expectedId = 1234;

    public function setUp() {
        $this->execution = m::mock('\ezcWorkflowExecution');
        $this->fixture = new \StarTribune\Workflow\Sniffer\ExecutionState;
        $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);
        $this->fixture->captureExecution($this->execution);
    }

    public function testIsCancelled() {
        $actual = $this->fixture->isCancelled();
        $this->assertEquals('cancel', $actual);
    }

    public function testHasEnded() {
        $actual = $this->fixture->hasEnded();
        $this->assertEquals('ended', $actual);
    }

    public function testIsSuspended() {
        $actual = $this->fixture->isSuspended();
        $this->assertEquals('suspended', $actual);
    }

    public function testGetVariables() {
        $actual = $this->fixture->getVariables();
        $this->assertEquals($this->expectedVariables, $actual);
    }

    public function testGetId() {
        $actual = $this->fixture->getId();
        $this->assertEquals($this->expectedId, $actual);
    }
}

In setUp(), we condition the mock object to return the values to be captured. Then we call the production captureExecution().

Then, each of the tests calls the appropriate production retrieval function, and we compare to the expected value. In this case all tests pass without changing any production code. Let’s finish out our test cases, writing production code as needed.

getVariables, when there are no variables, returns an empty array. We’ll skip this test because we know Zeta Workflow returns an array and we pass it through.

getVariable() returns the variable value or null if no such variable.

    public function testGetVariableMissingReturnsNull() {
        $actual = $this->fixture->getVariable('bogus');
        $this->assertTrue(null === $actual);
    }

    public function testGetVariableFirst() {
        $actual = $this->fixture->getVariable('one');
        $this->assertTrue(1 === $actual);
    }

    public function testGetVariableLast() {
        $actual = $this->fixture->getVariable('two');
        $this->assertTrue(2 === $actual);
    }

The production code:

    public function getVariable($key) {
        return (is_array($this->_variables) &&
                array_key_exists($key, $this->_variables)) ?
            $this->_variables[$key] : null;
    }

hasVariable() returns true or false.

    public function hasVariable($key) {
        return (is_array($this->_variables) &&
                array_key_exists($key, $this->_variables)) ?
            true : false;
    }

We have covered all our tests cases. We have a nice, tight, Execution State object with a clear single responsibility. Once we add the docblock comments, we’re done.

Next up: Execution State Registry.

Be the first to comment - What do you think?  Posted by admin - at 5:40 pm

Categories: Zeta Components Workflow   Tags:

Zeta Workflow 07: Execution Sniffer Plugin

Having now written the narrative documentation, it looks to me like it would be useful to have a plugin which captures the execution state after each node executes. I’m writing this walk through as I design and test this plugin.

Remember: I’m making this up as I go!

Features

I have these preliminary goals:

  • Understand the execution state of the current (most recently running) execution
  • Provide access to the current execution’s variables
  • Provide a list of all executions currently active. This goal is optional, being more an understanding/debugging aid than a production need

I picture the sniffer as being the passive go-between connecting the current Zeta Workflow thread to the Laravel wrapper (the Zeta Components Interface). The wrapper, in theory, only has access to the main thread. We could have any number of subflows, parallel executions, and so on. The wrapper has no way to sort this out short of diving in through the Zeta Workflow internals.

Take, for example, the case where a node decides to switch to another workflow. There is no such mechanism within Zeta Workflow. However, the node could return false, which suspends the workflow, and set an execution variable for the Sniffer to examine.

The Sniffer, in turn, could do something dramatic such as throw an exception. But since the workflow is already suspended, I think the passive approach is sufficient.

In this case the Laravel code which is running the workflow can query the Sniffer, with the Sniffer responding with a message to the effect of “Switch to workflow X.”

I also picture the Sniffer as being hidden entirely within the wrapper. The wrapper (or the Laravel Service Provider) instantiates the Sniffer, attaches it to the workflow execution, and handles any queries of the Sniffer.

We’ll need various tests ensuring that this mechanism actually works, for the main workflow, sub workflows, and parallel executions.

Tracing

As an added bonus, this might be a good mechanism for implementing a workflow event trace. However, an execution visualizer plugin already exists, documented at http://ezcomponents.org/docs/api/latest/Workflow/ezcWorkflowExecutionVisualizerPlugin.html.

That being the case, I’ll set aside the tracing idea for now. If I were to create a tracing mechanism, it looks like creating a separate plugin for that purpose is the way to go. That keeps the plugin in line with the Single Responsiblity Principle, and we only need to attach it to a workflow when tracing is needed.

Single Responsibility

The workflow can run a list of any number of plugins. Each plugin can attach itself to any number of events, or focus on a single specific event. For example, a plugin can be activated when the workflow suspends and when the workflow ends, but ignore all other events.

That being the case, it makes sense, when implementing plugins, to make a number of small plugins with a single clear responsibility or purpose, rather than a larger plugin with multiple responsibilities.

If we come up with a bunch of plugins, we might even want to create a plugin that attaches the plugins!

Sniffer Components

As a first cut, we can separate the Sniffer into the following components. Each component has a single well defined responsibility.

  • The plugin itself extends ezcWorkflowExecutionPlugin. The plugin communicates with the execution.
  • We maintain execution state with an Execution State object. Given the Zeta Workflow execution, the Execution State object captures and retains that execution’s current execution state. Our Sniffer will have multiple Execution State objects.
  • The Execution State Interpreter is closely tied to whatever message a node meant to send. For example, the Execution State Interpreter might pass back the information that we are now to switch to workflow X. This formalizes how we already announce the next slide to display. We check the execution variable ‘slide’.
  • The Execution Sniffer communicates with the wrapper. The Execution Sniffer is itself a composition bringing together the above components.

Since we have several components, we’ll create a new Sniffer directory and a corresponding tests/sniffer directory. We’ll define respective file namespaces accordingly.

Feature Details

Let’s lay out what each component should do. We’ll quickly discover that we’ll need another component which keeps track of the various Execution State objects. Call it the Execution State Registry.

The Plugin

The available methods are documented here: http://ezcomponents.org/docs/api/latest/Workflow/ezcWorkflowExecutionPlugin.html. For now let’s keep this simple and only implement afterNodeExecuted().

When called, we want to do the following.

  • Notify the Execution State Registry that this execution is, as of this moment, the current execution.
  • Obtain the relevant Execution State object from the Execution State Registry.
  • Have the relevant Execution State object capture and retain this execution’s current execution state.

Our unit test, therefore, can be frightfully simple. We can pass in mocks of the execution and node, and condition mocks of the Execution State Registry and Execution State object to ensure they are called as intended.

To be sure, we may have some frightfully complex scenarios to explore, such as synchronized parallel executions with paused sub workflows. But we’ll get to those later!

The Execution State Object

The execution state object retains the execution state as defined by isCancelled(), hasEnded(), and isSuspended(); and the execution variables as obtained via getVariables().

The functionality is:

  • Capture execution state, given the execution as passed to the plugin.
  • Disgorge the last-captured execution state. Mirror the execution methods isCancelled(), hasEnded(), isSuspended, getVariables(), getVariable(), hasVariable(), getId().

Our unit tests will explore the API of each method, followed by verifying the functionality in each case.

The Execution State Interpreter

We’ll keep the interpreter simple. The interpreter returns a text string. Here is the logic:

  1. If the ‘cancel’ variable exists, return “cancel”.
  2. If the ‘swap’ variable exists, it names the new workflow. Return “swap “ followed by the new workflow name. See the below discussion.
  3. If the ‘slide’ variable exists, return “slide “ followed by the slide variable’s value.
  4. Otherwise, return an empty string.

We could do a series of workflow swaps, for example, if our first workflow determines the customer’s demographics; we swap to a second workflow which determines the appropriate offer group; that offer group is a workflow which presents a specific offer.

Where this gets tricky is with sub workflows and parallel executions. So far as I know, if we are switching to a new workflow, the only thing we can do is cancel the current workflow, and proceed with the new workflow. The cancel() destroys all vestiges of the current workflow, including sub workflows and parallel threads.

Another option might be to leave the current workflow suspended, run the new workflow, and then resume the suspended workflow.

Another option might be that the workflow ends itself, presenting the new workflow name as its final result. We then proceed with the new workflow.

The Execution Sniffer

The Execution Sniffer communicates with the Zeta Components Interface (Wrapper). Either the Wrapper, or the Sniffer, is responsible for ensuring the plugin is attached to all workflows.

Since the Wrapper is responsible for instantiating workflow executions, I’m inclined to make the Wrapper responsible for attaching the plugin when it does so. But, as I think about that, I think we’ll make the Wrapper responsible for telling the Sniffer to attach its plugin.

We need to do a bit of homework. Once a plugin is attached to the main workflow, does it get attached to sub workflows and parallel threads?

Let’s check the sub workflow first. See zetacomponents/workflow/src/nodes/sub_workflow.php.

    public function execute( ezcWorkflowExecution $execution )
    {
        if ( $execution->definitionStorage === null )
        {
            throw new ezcWorkflowExecutionException(
              'No ezcWorkflowDefinitionStorage implementation available.'
            );
        }

        $workflow = $execution->definitionStorage->loadByName( $this->configuration['workflow'] );

        // Sub Workflow is not interactive.
        if ( !$workflow->isInteractive() && !$workflow->hasSubWorkflows() )
        {
            $subExecution = $execution->getSubExecution( null, false );
            $subExecution->workflow = $workflow;

            $this->passVariables(
              $execution, $subExecution, $this->configuration['variables']['in']
            );

            $subExecution->start();
        }

That’s enough code to tell us what we need to know. See line 15. We need to check $execution->getSubExecution( null, false ). On a side note, it looks like we can only use sub workflows if we are using the tied database. definitionStorage is the tied database component, and line three makes it mandatory for the Sub Workflow node.

GetSubExecution() is in zetacomponents/workflow/src/interfaces/execution.php, the same file containing start() and resume() we studied earlier.

    /**
     * Returns a new execution object for a sub workflow.
     *
     * If this method is used to resume a subworkflow you must provide
     * the execution id through $id.
     *
     * If $interactive is false an ezcWorkflowExecutionNonInteractive
     * will be returned.
     *
     * This method can be used by nodes implementing sub-workflows
     * to get a new execution environment for the subworkflow.
     *
     * @param  int $id
     * @param  bool $interactive
     * @return ezcWorkflowExecution
     * @ignore
     */
    public function getSubExecution( $id = null, $interactive = true )
    {
        if ( $interactive )
        {
            $execution = $this->doGetSubExecution( $id );
        }
        else
        {
            $execution = new ezcWorkflowExecutionNonInteractive;
        }

        foreach ( $this->plugins as $plugin )
        {
            $execution->addPlugin( $plugin );
        }

        return $execution;
    }

Line 31 is our answer. For each plugin in the parent execution (line 29), we add that plugin to the new sub execution (line 31).

What about parallel executions? I took a moment to chase that down as best I could from the online documentation, and it looks like we activate nodes in the current execution but do NOT create a new execution. So, it appears we have our plugin in place. If we ever do parallel threads, we’ll want to test and validate our assumption.

Homework complete, let’s lay out the Execution Sniffer features.

  • When requested by the wrapper, and given the execution, attach our plugin to the execution. attachPlugin()
  • Return the current Execution State Interpreter result. currentInterpretation()
  • Return execution state of the current Execution State object. currentIsCancelled(), currentHasEnded(), currentIsSuspended(), currentGetVariables(), currentGetVariable(), currentHasVariable(), currentGetId().

We’ll have the Laravel Service Provider wire everything together.

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.

Development Plan

Our general approach is to begin with the component with the least dependencies. We begin by writing tests exploring its interface (constructor and callable methods). We then drill down, fleshing out and verifying its functionality.

Having fully developed one component, we can accurately mock it while developing later components. Having fully unit tested it, we can be confident that mocking is sufficient.

We’ll develop our components in the following order, based on each component’s dependencies:

  1. The Execution State Object. Its only dependency is the execution, which we’ll mock.
  2. The Execution State Registry. Its only dependencies are Execution State Objects.
  3. The Execution State Interpreter. It acts on an Execution State Object.
  4. The Plugin. It works with the Execution State Registry and Execution State Objects.
  5. The Execution Sniffer. It is composed of each of the above objects.

Up next: The Execution State Object.

Be the first to comment - What do you think?  Posted by admin - at 1:27 pm

Categories: Zeta Components Workflow   Tags:

Zeta Workflow 06: Zeta Components Wrapper Implementation

This ZetaComponentsWrapper is a Composition bringing together multiple objects needed for using the Zeta Components Workflow:

  • $this->db, defined in the parent class. This the Tied Database connection. The Zeta Components Workflow Database Tie-in package makes a separate connection to our Laravel database, using the Zeta Components Database package.
  • $this->workflow. This the Zeta Components Workflow object as defined in the Zeta Components Workflow package.
  • $this->definition. This is the tied-database version of the Zeta Components Workflow definition. It is used to load and store the workflow object to/from the tied database.
  • $this->xmldefinition (not implemented). This is the xml-file version of the Zeta Components Workflow definition. It is used to load and store the workflow object to/from XML files.
  • $this->runner. An execution of the current workflow. This is the “interactive” execution, and is associated with the tied database.
  • $this->slide. A helper class that maps our slide names to their associated Workflow Action Nodes. This class simplifies the complexity of saying, “At this point in the flow I want to present this slide to the user.”
  • $this->node. A helper class that manipulates and builds up Workflow Nodes. This class simplifies various Workflow Node Composition chores.

Be the first to comment - What do you think?  Posted by admin - at 1:09 pm

Categories: Zeta Components Workflow   Tags:

Zeta Workflow 05: Zeta Components Interface

When using the Zeta Components package from Laravel, you will be using the Zeta Components Wrapper. In PHP (and Laravel) terms, the Zeta Components Wrapper implements the Zeta Components Interface specification. The rest of this post is a copy of the PHPDocumentor documentation inside the Zeta Components Interface.

  • The Zeta Components Interface is file workbench/star-tribune/workflow/src/StarTribune/Workflow/Wrapper/ZetaComponentsInterface.php. 
  • The PHPDocumentor-generated file is workbench/star-tribune/workflow/phpdoc/classes/StarTribune.Workflow.Wrapper.ZetaComponentsInterface.html.
  • The top of the phpdoc tree is workbench/star-tribune/workflow/phpdoc/index.html.

Read more…

Be the first to comment - What do you think?  Posted by admin - at 12:26 pm

Categories: Zeta Components Workflow   Tags:

Zeta Workflow 04: Exception Handling Walkthrough

The released Zeta Workflow code does not correctly handle exceptions. This walkthrough describes the behavior as modified by myself.

The basic issue was that both start() and resume(), when using the Database Tie-In component, issue beginTransaction() to the database engine. The workflows can throw exceptions which remain uncaught. Because the exception is not caught within the workflow code, the transaction is never closed with either rollback or commit. When using an sqlite database, unit tests then fail, complaining of a locked table.

Within start() and resume(), we now catch the exceptions so that we can commit or roll back the transaction before (possibly) re-throwing the exception.

Read more…

Be the first to comment - What do you think?  Posted by admin - March 28, 2014 at 7:30 pm

Categories: Zeta Components Workflow   Tags:

Zeta Workflow 03: Failure Handling Walkthrough

Here we look at the nuances of start() and resume(). We walk through several components along the way.

Let’s take my usual test-first approach. Our aim is to characterize and verify Zeta Workflow’s behavior in failure scenarios.

Failure vs. Exception

Draw a careful distinction between “failure” and “exception”:

  • A “failure” is when the node (or its service object) returns false indicating the workflow should NOT proceed.
  • An “exception” is when the node (or its service object) throws an Exception.

As I developed the package, my primary use case has been “At this point, tell Laravel to present slide X.” We implement this by using the SlideProvider callback you have already seen. We’ll continue this approach by using the same type of scenario, but condition SlideProvider to return false, telling Zeta Workflow something failed.

The Unit Test

Our unit test tests/rollback/slideFailTest.php begins:

namespace StarTribune\Workflow\Tests\Workflow;

require_once __DIR__.'/../DbTestCase.php';
use StarTribune\Workflow\Wrapper\ZetaComponentsWrapper;
use StarTribune\Workflow\Wrapper\ZetaComponentsInterface;
class slideFailTest extends \StarTribune\Workflow\Tests\DbTestCase {

We already have lots to talk about!

Read more…

Be the first to comment - What do you think?  Posted by admin - at 6:30 pm

Categories: Zeta Components Workflow   Tags:

Zeta Workflow 02: Failure Handling

Here is what you need to know about workflow failure and exception handling:

  • If you resume the workflow execution after the failure, it will resume with the failing node.
  • Zeta Workflow has no provision for skipping a failing node and resuming execution at the next node. If you are stuck, you are stuck, and that is that.
  • When the workflow fails and/or throws an exception, state should be saved up to the point of the failure.

Read more…

Be the first to comment - What do you think?  Posted by admin - at 6:07 pm

Categories: Zeta Components Workflow   Tags:

Zeta Workflow 01: Workflow Communication

Execution Variables

One way to communicate with the workflow is via execution variables. You can use getVariable(), setVariable(), hasVariable(), etc. to work with the execution variables. My tests show the execution variables do get correctly persisted with the database tie-in package.

Unfortunately it’s not that simple. To explain, I need to throw out a lot of details about how executions work.

Read more…

Be the first to comment - What do you think?  Posted by admin - at 4:43 pm

Categories: Zeta Components Workflow   Tags:

« Previous PageNext Page »