Zeta Workflow 13: Sniffer Integration

What’s left?

  1. When con­struct­ing the Zeta Com­po­nents Wrap­per, include the Sniffer.
  2. When the wrap­per starts or resumes an exe­cu­tion, attach the Snif­fer Plugin.
  3. Add each of the Sniffer’s cur­rent*() meth­ods to the wrap­per inter­face and implementation.
  4. Copy and mod­ify the sub work­flow test. Change the new (copied) test to use cur­rentIn­ter­pre­ta­tion() in the slide ver­i­fi­ca­tion. This should tell us that every­thing is inte­grated and work­ing correctly.
  5. Add docblock com­ments every­where. “make php­doc” will tell us of any­thing missing.
  6. Run all unit tests (make all).
  7. Add, com­mit, and push the code.

Hav­ing done the above except com­mit the code, I find that the inte­gra­tion test fails in a big way.

A bit of inves­ti­ga­tion turns up the prob­lem. We are hit­ting our own excep­tion for see­ing a null exe­cu­tion id in the sub work­flow. We’ll add that to the inter­preter so it’s clear what happened.

In Exe­cu­tion­StateReg­istry, if the id is null, we now set the id (our inter­nal reg­istry index) to ‘null id’. This will break the unit test which checks for null exception.

The sub­Work­flowTest dis­tin­guishes between the main work­flow execution’s slide, and the “cur­rent” or “active” sub work­flow slide. Since we’re now using the inter­preter, we only want the cur­rent slide. Adjust the new snif­fer sub work­flow test accord­ingly. Now the test passes.

Every­thing runs cleanly!

Be the first to comment - What do you think?  Posted by admin - March 30, 2014 at 10:12 am

Categories: Zeta Components Workflow   Tags:

Zeta Workflow 12: The Sniffer Class

Here are the Exe­cu­tion Snif­fer features.

  • When requested by the wrap­per, and given the exe­cu­tion, attach our plu­gin to the exe­cu­tion. attachPlugin()
  • Return the cur­rent Exe­cu­tion State Inter­preter result. currentInterpretation()
  • Return exe­cu­tion state of the cur­rent Exe­cu­tion State object. cur­ren­tIs­Can­celled(), cur­ren­tHasEnded(), cur­ren­tIs­Sus­pended(), cur­rent­Get­Vari­ables(), cur­rent­Get­Vari­able(), cur­ren­tHas­Vari­able(), currentGetId().

We’ll have the Lar­avel Ser­vice Provider wire every­thing together.

Test Out­comes

Here is our list of tests.

  1. get­Plu­gin() returns the plu­gin object which was wired up by the ser­vice provider.
  2. get­Inter­preter() returns the inter­preter object which was wired up by the ser­vice provider.
  3. getReg­istry() returns the reg­istry object which was wired up by the ser­vice provider.
  4. attachPlugin($execution) calls $execution->addPlugin() with our plu­gin as wired up by the ser­vice provider.
  5. cur­rentIn­ter­pre­ta­tion() calls $registry->getCurrentExecutionState() and returns mock $state, and calls $interpreter->interpretExecutionState() with mock $state and returns ‘what­ever’, and the cur­rentIn­ter­pre­ta­tion() call returns the same ‘whatever’.
  6. cur­ren­tIs­Can­celled(), cur­ren­tHasEnded(), cur­ren­tIs­Sus­pended(), cur­rent­Get­Vari­ables(), cur­rent­Get­Vari­able(), cur­ren­tHas­Vari­able(), cur­rent­GetId() each call $registry->getCurrentExecutionState which returns mock $state; mock $state gets the rel­e­vant method called, and that method’s return value is the return value for the cur­rent*() retrieval methods.

Ser­vice Provider

The tricky part is get­ting every­thing wired up cor­rectly in the Ser­vice Provider. Here is the first test of get­Plu­gin(), plus var­i­ous tests ensur­ing we have our mocks set up correctly.

namespace StarTribune\Workflow\Tests\Sniffer;
use Mockery as m;
class snifferTest extends \TestCase {
    public $execution;
    public $registry;
    public $fixture;
    public $interpreter;
    public $plugin;

    public function setUp() {
        parent::setUp();
        $this->plugin      = m::mock('\StarTribune\Workflow\Sniffer\Plugin');
        $this->interpreter = m::mock('\StarTribune\Workflow\Sniffer\ExecutionStateInterpreter');
        $this->registry    = m::mock('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry');
        $this->plugin->shouldReceive('getRegistry')->andReturn($this->registry);
        $this->execution   = m::mock('\ezcWorkflowExecution');

        \App::instance('workflow.snifferplugin',                                  $this->plugin);
        \App::instance('\StarTribune\Workflow\Sniffer\ExecutionStateInterpreter', $this->interpreter);
        \App::instance('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry',    $this->registry);

        $this->fixture     = \App::make('workflow.sniffer');
    }

    public function testMockClass() {
        $this->assertTrue($this->plugin      instanceof \StarTribune\Workflow\Sniffer\Plugin);
        $this->assertTrue($this->interpreter instanceof \StarTribune\Workflow\Sniffer\ExecutionStateInterpreter);
        $this->assertTrue($this->registry    instanceof \StarTribune\Workflow\Sniffer\ExecutionStateRegistry);
    }

    public function testSamePlugin() {
        $actual = \App::make('workflow.snifferplugin');
        $this->assertSame($this->plugin, $actual);
    }

    public function testSameInterpreter() {
        $actual = \App::make('\StarTribune\Workflow\Sniffer\ExecutionStateInterpreter');
        $this->assertSame($this->interpreter, $actual);
    }

    public function testSameRegistry() {
        $actual = \App::make('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry');
        $this->assertSame($this->registry, $actual);
    }

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

    public function testgetPluginReturnsPlugin() {
        $actual = $this->fixture->getPlugin();
        $this->assertSame($this->plugin, $actual);
    }
}

Lines 12–16 set up our mock objects. Inside the ser­vice provider, I needed to ensure we pass around the same reg­istry object. You’ll see how we do that in a moment. In short, what we do, is cre­ate the reg­istry object, pass it in to the plu­gin object, and then ask the plu­gin object to give it back to us.

Lines 18–20 reg­is­ter our mocks with the Lar­avel Inver­sion of Con­trol (IoC) container.

Line 22 cre­ates the snif­fer object as our test fix­ture. We have to do this last, because the ser­vice provider wires together the var­i­ous objects, and we need to reg­is­ter them on lines 18–20 to ensure that our test fix­ture incor­po­rates our mock objects.

The first test, lines 25–29, ensures we got our class names cor­rect in the mock objects. This test won’t often fail unless you mess up the ini­tial copy/paste stuff.

The next three tests (test­Same­Plu­gin, test­SameIn­ter­preter, test­SameReg­istry) ensure that we get the same mock object every time we ask for the object. I wrote these tests before touch­ing the Lar­avel Ser­vice Provider. That way, as I mess with things in the ser­vice provider, I can be sure this aspect did not break.

Line 46 is my usual boil­er­plate test ensur­ing the test fix­ture is what I think it should be.

Finally, lines 50–53 will only pass once we have the Ser­vice Provider wiring every­thing up correctly.

Here is the Snif­fer class:

namespace StarTribune\Workflow\Sniffer;
class Sniffer {
    private $_plugin;
    private $_interpreter;
    private $_registry;

    public function __construct(\StarTribune\Workflow\Sniffer\Plugin $plugin,
                                \StarTribune\Workflow\Sniffer\ExecutionStateInterpreter $interpreter,
                                \StarTribune\Workflow\Sniffer\ExecutionStateRegistry $registry) {
        $this->_plugin = $plugin;
        $this->_interpreter = $interpreter;
        $this->_registry = $registry;
    }

    public function getPlugin() {
        return $this->_plugin;
    }
}

Here are the rel­e­vant parts of the ser­vice provider:

	public function register()
	{
        $this->registerPurgeExpiredCommand();
        $this->commands('workflow.purgeexpired');
        $this->registerCleanVersionCommand();
        $this->commands('workflow.cleanversion');
        $this->registerDeleteByNameCommand();
        $this->commands('workflow.deletebyname');
        $this->bindZetaComponentsInterface();
        $this->bindSnifferPlugin();
        $this->bindSniffer();
	}

    public function bindSnifferPlugin() {
        $this->app['workflow.snifferplugin'] = $this->app->share(function($app){
            $registry = $this->app->make('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry');
            $plugin   = $this->app->make('\StarTribune\Workflow\Sniffer\Plugin', array($registry));
            return $plugin;
        });
    }

    public function bindSniffer() {
        $this->app['workflow.sniffer'] = $this->app->share(function($app){
            $plugin = $this->app->make('workflow.snifferplugin');
            $registry = $plugin->getRegistry();
            $interpreter = $this->app->make('\StarTribune\Workflow\Sniffer\ExecutionStateInterpreter');
            $implementation = new \StarTribune\Workflow\Sniffer\Sniffer($plugin, $interpreter, $registry);
            return $implementation;
        });
    }

	public function provides()
	{
		return array('workflow.purgeexpired', 'workflow.cleanversion', 'workflow.deletebyname',
                     'workflow.snifferplugin', 'workflow.sniffer');
	}

The Ser­vice Provider took some fig­ur­ing out. I’ve not found clear doc­u­men­ta­tion on exactly how to do this.

The Snif­fer has three depen­den­cies: plu­gin, inter­preter, reg­istry. How­ever, we also pass reg­istry as a depen­dency to plugin.

If you’re pass­ing a depen­dency into the con­struc­tor, the way to do that is in the Ser­vice Provider.

That means we need to cre­ate the plu­gin in the ser­vice provider (it has a depen­dency to pass in), and we need to cre­ate the snif­fer in the ser­vice provider (it has three depen­den­cies to pass in). That means we have two sep­a­rate clo­sures, one which cre­ates the plu­gin and one which cre­ates the sniffer.

The tricky part is that both the plu­gin and the snif­fer need to receive the SAME reg­istry object.

The way I solved the prob­lem is to cre­ate the plu­gin, inject­ing the reg­istry, and then allow the plu­gin to dis­gorge that same reg­istry object.

On lines 14–20 we cre­ate the reg­istry and plu­gin. The plu­gin now con­tains a ref­er­ence to the registry.

On lines 22–30 we cre­ate the sniffer.

Line 24 cre­ates the plu­gin using the pre­vi­ous clo­sure (lines 14–19). Line 15 reg­is­tered the clo­sure as ‘workflow.snifferplugin’, and now on line 24 we ask for ‘workflow.snifferplugin’.

Next, we take advan­tage of the fact that the plu­gin also cre­ated the reg­istry. On line 25 we ask for that already-constructed object.

Line 26 instan­ti­ates the inter­preter object.

Now, on line 27, we cre­ate the snif­fer. We pass in the plu­gin, inter­preter, and reg­istry objects. We now know that both the plu­gin and the snif­fer ref­er­ence the SAME reg­istry object, which was the whole point of the exercise.

Mean­while, we have the already-seen unit tests which val­i­date that every­thing took place as we just described.

With the tricky part done, the rest of our tests should be quite straight­for­ward. I’ll work back and forth between test case and pro­duc­tion code. Hav­ing done so, here is the test class fol­lowed by the pro­duc­tion Snif­fer class.

In devel­op­ing the addPlu­gin test, I checked the Zeta Work­flow source code and dis­cov­ered something:

    /**
     * Adds a plugin to this execution.
     *
     * @param ezcWorkflowExecutionPlugin $plugin
     * @return bool true when the plugin was added, false otherwise.
     */
    public function addPlugin( ezcWorkflowExecutionPlugin $plugin )
    {
        $pluginClass = get_class( $plugin );

        if ( !isset( $this->plugins[$pluginClass] ) )
        {
            $this->plugins[$pluginClass] = $plugin;

            return true;
        }
        else
        {
            return false;
        }
    }

We can add some test cases:

  1. Adding a plu­gin class the first time returns true.
  2. Adding the same plu­gin class a sec­ond time returns false.

The test class:

class snifferTest extends \TestCase {
    public $execution;
    public $registry;
    public $fixture;
    public $interpreter;
    public $plugin;
    public $state;

    public function setUp() {
        parent::setUp();
        $this->plugin      = m::mock('\StarTribune\Workflow\Sniffer\Plugin');
        $this->interpreter = m::mock('\StarTribune\Workflow\Sniffer\ExecutionStateInterpreter');
        $this->registry    = m::mock('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry');
        $this->plugin->shouldReceive('getRegistry')->andReturn($this->registry);
        $this->state       = m::mock('\StarTribune\Workflow\Sniffer\ExecutionState');
        $this->execution   = m::mock('\ezcWorkflowExecution');

        \App::instance('workflow.snifferplugin',                                  $this->plugin);
        \App::instance('\StarTribune\Workflow\Sniffer\ExecutionStateInterpreter', $this->interpreter);
        \App::instance('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry',    $this->registry);

        $this->fixture     = \App::make('workflow.sniffer');
    }

    public function testMockClass() {
        $this->assertTrue($this->plugin      instanceof \StarTribune\Workflow\Sniffer\Plugin);
        $this->assertTrue($this->interpreter instanceof \StarTribune\Workflow\Sniffer\ExecutionStateInterpreter);
        $this->assertTrue($this->registry    instanceof \StarTribune\Workflow\Sniffer\ExecutionStateRegistry);
    }

    public function testSamePlugin() {
        $actual = \App::make('workflow.snifferplugin');
        $this->assertSame($this->plugin, $actual);
    }

    public function testSameInterpreter() {
        $actual = \App::make('\StarTribune\Workflow\Sniffer\ExecutionStateInterpreter');
        $this->assertSame($this->interpreter, $actual);
    }

    public function testSameRegistry() {
        $actual = \App::make('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry');
        $this->assertSame($this->registry, $actual);
    }

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

    public function testgetPluginReturnsPlugin() {
        $actual = $this->fixture->getPlugin();
        $this->assertSame($this->plugin, $actual);
    }

    public function testgetInterpreterReturnsInterpreter() {
        $actual = $this->fixture->getInterpreter();
        $this->assertSame($this->interpreter, $actual);
    }

    public function testgetRegistryReturnsRegistry() {
        $actual = $this->fixture->getRegistry();
        $this->assertSame($this->registry, $actual);
    }

    public function testattachPlugin() {
        $this->execution->shouldReceive('addPlugin')
            ->once()
            ->with($this->plugin)
            ->andReturn(true);
        $result = $this->fixture->attachPlugin($this->execution);
        $this->assertTrue(true === $result);
    }

    public function testreattachPlugin() {
        $this->execution->shouldReceive('addPlugin')
            ->times(2)
            ->with($this->plugin)
            ->andReturn(true, false);
        $result1 = $this->fixture->attachPlugin($this->execution);
        $this->assertTrue(true === $result1);
        $result2 = $this->fixture->attachPlugin($this->execution);
        $this->assertTrue(false === $result2);
    }

    public function testcurrentInterpretation() {
        $expected = 'whatever';
        $this->registry->shouldReceive('getCurrentExecutionState')
            ->andReturn($this->state);
        $this->interpreter->shouldReceive('interpretExecutionState')
            ->with($this->state)
            ->andReturn($expected);
        $actual = $this->fixture->currentInterpretation();
        $this->assertEquals($expected, $actual);
    }

    /**
     * @dataProvider dataCurrent
     */
    public function testcurrentIsCancelled($function1, $function2) {
        $expected = 'return_'.$function1;
        $this->registry->shouldReceive('getCurrentExecutionState')
            ->andReturn($this->state);
        $this->state->shouldReceive($function1)
            ->once()
            ->andReturn($expected);
        $actual = $this->fixture->$function2();
        $this->assertEquals($expected, $actual);
    }

    public function dataCurrent() {
        $data = array();
        $data[] = array('isCancelled', 'currentIsCancelled');
        $data[] = array('hasEnded', 'currentHasEnded');
        $data[] = array('isSuspended', 'currentIsSuspended');
        $data[] = array('getId', 'currentGetId');
        $data[] = array('getVariables', 'currentGetVariables');

        return $data;
    }

    public function testGetVariable() {
        $expected = 'theValue';
        $key = 'theName';
        $this->registry->shouldReceive('getCurrentExecutionState')
            ->andReturn($this->state);
        $this->state->shouldReceive('getVariable')
            ->with($key)
            ->once()
            ->andReturn($expected);
        $actual = $this->fixture->currentGetVariable($key);
        $this->assertEquals($expected, $actual);
    }

    public function testHasVariable() {
        $expected = false;
        $key = 'theName';
        $this->registry->shouldReceive('getCurrentExecutionState')
            ->andReturn($this->state);
        $this->state->shouldReceive('hasVariable')
            ->with($key)
            ->once()
            ->andReturn($expected);
        $actual = $this->fixture->currentHasVariable($key);
        $this->assertTrue($expected === $actual);
    }
}

The pro­duc­tion Snif­fer class:

namespace StarTribune\Workflow\Sniffer;
class Sniffer {
    private $_plugin;
    private $_interpreter;
    private $_registry;

    public function __construct(\StarTribune\Workflow\Sniffer\Plugin $plugin,
                                \StarTribune\Workflow\Sniffer\ExecutionStateInterpreter $interpreter,
                                \StarTribune\Workflow\Sniffer\ExecutionStateRegistry $registry) {
        $this->_plugin = $plugin;
        $this->_interpreter = $interpreter;
        $this->_registry = $registry;
    }

    public function getPlugin() {
        return $this->_plugin;
    }

    public function getInterpreter() {
        return $this->_interpreter;
    }

    public function getRegistry() {
        return $this->_registry;
    }

    public function attachPlugin(\ezcWorkflowExecution $execution) {
        return $execution->addPlugin($this->getPlugin());
    }

    public function currentInterpretation() {
        $state = $this->getRegistry()->getCurrentExecutionState();
        return $this->getInterpreter()->interpretExecutionState($state);
    }

    public function currentIsCancelled() {
        return $this->getCurrent('isCancelled');
    }

    public function currentHasEnded() {
        return $this->getCurrent('hasEnded');
    }

    public function currentIsSuspended() {
        return $this->getCurrent('isSuspended');
    }

    public function currentGetId() {
        return $this->getCurrent('getId');
    }

    public function currentGetVariables() {
        return $this->getCurrent('getVariables');
    }

    public function currentGetVariable($key) {
        return $this->getCurrentKey('getVariable', $key);
    }

    public function currentHasVariable($key) {
        return $this->getCurrentKey('hasVariable', $key);
    }

    protected function getCurrent($function) {
        $state = $this->getRegistry()->getCurrentExecutionState();
        return $state->$function();
    }

    protected function getCurrentKey($function, $key) {
        $state = $this->getRegistry()->getCurrentExecutionState();
        return $state->$function($key);
    }
}

Next up: Snif­fer Integration

Be the first to comment - What do you think?  Posted by admin - at 10:11 am

Categories: Zeta Components Workflow   Tags:

Zeta Workflow 11: Sniffer Plugin

The avail­able meth­ods are doc­u­mented here: http://ezcomponents.org/docs/api/latest/Workflow/ezcWorkflowExecutionPlugin.html. For now let’s keep this sim­ple and only imple­ment afterNodeExecuted().

When called, we want to do the following.

  • Notify the Exe­cu­tion State Reg­istry that this exe­cu­tion is, as of this moment, the cur­rent execution.
  • Obtain the rel­e­vant Exe­cu­tion State object from the Exe­cu­tion State Registry.
  • Have the rel­e­vant Exe­cu­tion State object cap­ture and retain this execution’s cur­rent exe­cu­tion state.

Our unit test, there­fore, can be fright­fully sim­ple. We can pass in mocks of the exe­cu­tion and node, and con­di­tion mocks of the Exe­cu­tion State Reg­istry and Exe­cu­tion State object to ensure they are called as intended.

To be sure, we may have some fright­fully com­plex sce­nar­ios to explore, such as syn­chro­nized par­al­lel exe­cu­tions with paused sub work­flows. But we’ll get to those later!

Test Out­comes

Hav­ing now built and tested the Exe­cu­tion State Reg­istry, it’s clear that we only need to call the registry’s setCur­rentEx­e­cu­tion() method. This means we’ll need to pass the reg­istry object in to the plugin’s constructor.

A check of the Zeta Work­flow source code http://ezcomponents.org/docs/api/latest/__filesource/fsource_Workflow—Workflow—1.4—src—interfaces—execution_plugin.php.html shows that we’ll be extend­ing the abstract base class, and that it does not define a con­struc­tor. So we’re safe defin­ing our con­struc­tor to include the reg­istry object.

For our only test, we will con­di­tion a mock reg­istry object to require that its setCur­rentEx­e­cu­tion() method is called with our mock exe­cu­tion object.

Here is the test.

namespace StarTribune\Workflow\Tests\Sniffer;
use Mockery as m;
class snifferPluginTest extends \TestCase {
    public $execution;
    public $registry;
    public $fixture;
    public $node;

    public function setUp() {
        parent::setUp();
        $this->registry  = m::mock('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry');
        $this->execution = m::mock('\ezcWorkflowExecution');
        $this->node      = m::mock('\ezcWorkflowNode');
        $this->fixture   = new \StarTribune\Workflow\Sniffer\Plugin($this->registry);
    }

    public function testMockCorrectClass() {
        $this->assertTrue($this->registry  instanceof \StarTribune\Workflow\Sniffer\ExecutionStateRegistry);
        $this->assertTrue($this->execution instanceof \ezcWorkflowExecution);
        $this->assertTrue($this->node      instanceof \ezcWorkflowNode);
    }

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

    public function testSetCurrentExecution() {
        $this->registry->shouldReceive('setCurrentExecution')
            ->once()
            ->with($this->execution);
        $this->fixture->afterNodeExecuted($this->execution, $this->node);
    }

}

Here is the pro­duc­tion code. As the start­ing point, I copied the method docblock and sig­na­ture from the abstract par­ent class.

namespace StarTribune\Workflow\Sniffer;

class Plugin extends \ezcWorkflowExecutionPlugin {
    protected $registry;

    public function __construct(\StarTribune\Workflow\Sniffer\ExecutionStateRegistry $registry) {
        $this->registry = $registry;
    }

    /**
     * Called after a node has been executed.
     *
     * @param ezcWorkflowExecution $execution
     * @param ezcWorkflowNode      $node
     */
    public function afterNodeExecuted( \ezcWorkflowExecution $execution, \ezcWorkflowNode $node )
    {
        $ignore = $this->registry->setCurrentExecution($execution);
    }
}

Once again we have a small class with a sin­gle responsibility.

Next up: The Snif­fer Class.

Be the first to comment - What do you think?  Posted by admin - at 9:14 am

Categories: Zeta Components Workflow   Tags:

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 pri­or­ity order.

The class has only a sin­gle method, interpretExecutionState(ExecutionState $state). The object does not need to cache any­thing; it sim­ply inter­prets the passed-in exe­cu­tion state object, return­ing a string as the interpretation.

Given that our unit tests already inter­pret the slide vari­able, we can adapt one of those tests to exer­cise the inter­preter. Once inter­preter devel­op­ment is com­plete (with unit tests), make a copy of the subWorkflowTest.php. Change the copy’s ver­i­fyS­lide method to use the inter­preter. All sub work­flow tests should con­tinue to pass while using the Exe­cu­tion State Interpreter.

Oops, I got ahead of myself. We’ll adapt a copy of the sub­Work­flowTest once the entire snif­fer is com­plete and is get­ting attached to the workflow.

Test Out­comes

Given the above sim­plis­tic inter­pre­ta­tion rules, we have eight pos­si­ble out­comes depend­ing on whether the can­cel, swap, slide vari­ables do or do not exist. We can set up a sin­gle test with a data provider dri­ving the eight scenarios.

In the data provider, we’ll use null to indi­cate the vari­able does not exist, and its value oth­er­wise. We can con­di­tion a mock Exe­cu­tion­State object.

After the sev­eral iter­a­tions flesh­ing out the tests and pro­duc­tion 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 pro­duc­tion 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 Snif­fer 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 poten­tially have mul­ti­ple exe­cu­tions, we need a reg­istry to man­age the mul­ti­ple Exe­cu­tion State objects. The reg­istry also main­tains the notion of which is the “cur­rent” Exe­cu­tion State object. The fea­tures are:

  • Accept noti­fi­ca­tion that “this” is the cur­rent execution.
  • For a given exe­cu­tion, return the rel­e­vant Exe­cu­tion State object. We either fetch it from our cache, or con­struct it on the spot. I am assum­ing that, by this point, each exe­cu­tion has an id. If not, ini­tially, throw an excep­tion. Later, we should prob­a­bly ignore exe­cu­tions with no id, assum­ing that later in the work­flow exe­cu­tion it WILL have an id, and we can snag it at that point.
  • Return the “cur­rent” Exe­cu­tion State object.

Test Out­comes

For the fol­low­ing tests, $exe­cu­tion is a Zeta Work­flow exe­cu­tion object and $exe­cu­tion­State is an Exe­cu­tion State object. We con­sider unique­ness accord­ing to $execution->getId() and the cor­re­spond­ing $executionState->getId().

  1. getExecutionState($execution) returns an ini­tial­ized Exe­cu­tion State object. The object is ini­tial­ized via captureExecution().
  2. 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.
  3. 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.
  4. 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.
  5. If $execution->getId() returns null, we throw an exception.
  6. 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.

For the test fix­ture, we’ll use mock exe­cu­tion objects and real Exe­cu­tion 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 pro­duc­tion 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 pro­duc­tion 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 pro­duc­tion 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 excep­tion. 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");
    }

Pro­duc­tion 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 “dif­fer­ent exe­cu­tion returns dif­fer­ent” set up and run the whole thing as a sin­gle 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 pro­duc­tion 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 Exe­cu­tion State Inter­preter.

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 exe­cu­tion state object retains the exe­cu­tion state as defined by isCan­celled(), hasEnded(), and isSus­pended(); and the exe­cu­tion vari­ables as obtained via getVariables().

The func­tion­al­ity is:

  • Cap­ture exe­cu­tion state, given the exe­cu­tion as passed to the plugin.
  • Dis­gorge the last-captured exe­cu­tion state. Mir­ror the exe­cu­tion meth­ods isCan­celled(), hasEnded(), isSus­pended, get­Vari­ables(), get­Vari­able(), has­Vari­able(), getId().

Our unit tests will explore the API of each method, fol­lowed by ver­i­fy­ing the func­tion­al­ity in each case.

Start­ing Point

We begin with a list of unit test out­comes. I’m mak­ing this list off the top of my head based on the above descrip­tion. We may add or change tests as we go:

  1. Can cre­ate a mock Zeta Work­flow Exe­cu­tion object.
  2. The Zeta Work­flow Exe­cu­tion object is an instance of the cor­rect class, ezcWorkflowExecution.
  3. Can cap­ture isCancelled().
  4. Can cap­ture hasEnded().
  5. Can cap­ture isSuspended().
  6. Can cap­ture get­Vari­ables(). get­Vari­able() and has­Vari­able are based on get­Vari­ables() con­tent, so there is no need for sep­a­rate cap­ture mechanisms.
  7. Can cap­ture getId().
  8. A new cap­ture com­pletely replaces any pre­vi­ous capture.
  9. captureExecution($execution) cap­tures the above items 3–7.
  10. If any of the retrieval func­tions is called before the exe­cu­tion was cap­tured, the retrieval func­tion returns null.
  11. The retrieval func­tions return their respec­tive cap­tured isCan­celled(), hasEnded(), isSus­pended(), get­Vari­ables(), getId().
  12. get­Vari­ables(), when there are no vari­ables, returns empty array.
  13. get­Vari­able(), if the vari­able does not exist in the cur­rent cap­ture, returns null.
  14. get­Vari­able(), if the vari­able does exist in the cur­rent cap­ture, returns the variable’s value.
  15. has­Vari­able() returns true or false for whether or not that vari­able exists in the capture.

We start cod­ing by:

  1. Cre­ate code direc­tory Snif­fer and test direc­tory 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 tar­get in the Makefile.
  5. Cre­ate a new test sniffer/executionStateTest.php which extends Test­Case. We do not need to extend the data­base test case.
  6. Cre­ate a fail­ing test with $this->fail(‘here’). Run the Make­file and observe the test failure.
  7. Pro­ceed with the first of the above test outcomes.

Here is the ini­tial 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 Make­file and phpunit.xml cor­rectly 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 run­ning the tests we think we are, we can proceed.

Mock Object

We start with a cou­ple of tests ensur­ing we get our mock objects set up correctly.

In each case, we start by observ­ing the test fail­ing, then watch it pass. With the first test, we comment-out the assert to ensure that the expec­ta­tion fails for not being met. In the sec­ond test, we just let it ride; it’s triv­ial. But it’s impor­tant to have the test because it “nails down” the exe­cu­tion 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 Cap­ture Methods

We write the tests first. In this case we only con­firm that the (not yet exis­tent) func­tions are callable. When we get to the retrieval func­tions, we’ll flesh out the actual cap­ture code.

The first test requires that we cre­ate 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 refac­tor the con­struc­tion 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 cap­ture tests pass.

The first pair of cap­ture 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 writ­ten it, we can see the first of the two tests is redun­dant. Any pur­pose served by the first test is served by the second.

Here is the class as writ­ten to pass the above tests.

class ExecutionState {
    public $isCancelled;

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

The rest of the cap­tures and pro­duc­tion code will be sim­i­lar. 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 pro­duc­tion 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 Func­tions

We are skip­ping the rule “a new cap­ture com­pletely replaces the old cap­ture”. It will be clear from inspect­ing the pro­duc­tion code that this is the case, and I don’t think the test is suf­fi­ciently worth the time to write it.

Let’s test that any retrieval prior to the first cap­ture 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 cap­tured prop­er­ties from pub­lic to pri­vate. Here is the new pro­duc­tion 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 infor­ma­tion, we should go to a dif­fer­ent test fix­ture setup. This means start­ing a new test class. Here is tests/sniffer/executionStateRetrievalTest.php, with the retrieval func­tions 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 con­di­tion the mock object to return the val­ues to be cap­tured. Then we call the pro­duc­tion captureExecution().

Then, each of the tests calls the appro­pri­ate pro­duc­tion retrieval func­tion, and we com­pare to the expected value. In this case all tests pass with­out chang­ing any pro­duc­tion code. Let’s fin­ish out our test cases, writ­ing pro­duc­tion code as needed.

get­Vari­ables, when there are no vari­ables, returns an empty array. We’ll skip this test because we know Zeta Work­flow returns an array and we pass it through.

get­Vari­able() returns the vari­able 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 pro­duc­tion code:

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

has­Vari­able() returns true or false.

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

We have cov­ered all our tests cases. We have a nice, tight, Exe­cu­tion State object with a clear sin­gle respon­si­bil­ity. Once we add the docblock com­ments, we’re done.

Next up: Exe­cu­tion State Reg­istry.

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

Hav­ing now writ­ten the nar­ra­tive doc­u­men­ta­tion, it looks to me like it would be use­ful to have a plu­gin which cap­tures the exe­cu­tion state after each node exe­cutes. I’m writ­ing this walk through as I design and test this plugin.

Remem­ber: I’m mak­ing this up as I go!

Fea­tures

I have these pre­lim­i­nary goals:

  • Under­stand the exe­cu­tion state of the cur­rent (most recently run­ning) execution
  • Pro­vide access to the cur­rent execution’s variables
  • Pro­vide a list of all exe­cu­tions cur­rently active. This goal is optional, being more an understanding/debugging aid than a pro­duc­tion need

I pic­ture the snif­fer as being the pas­sive go-between con­nect­ing the cur­rent Zeta Work­flow thread to the Lar­avel wrap­per (the Zeta Com­po­nents Inter­face). The wrap­per, in the­ory, only has access to the main thread. We could have any num­ber of sub­flows, par­al­lel exe­cu­tions, and so on. The wrap­per has no way to sort this out short of div­ing in through the Zeta Work­flow internals.

Take, for exam­ple, the case where a node decides to switch to another work­flow. There is no such mech­a­nism within Zeta Work­flow. How­ever, the node could return false, which sus­pends the work­flow, and set an exe­cu­tion vari­able for the Snif­fer to examine.

The Snif­fer, in turn, could do some­thing dra­matic such as throw an excep­tion. But since the work­flow is already sus­pended, I think the pas­sive approach is sufficient.

In this case the Lar­avel code which is run­ning the work­flow can query the Snif­fer, with the Snif­fer respond­ing with a mes­sage to the effect of “Switch to work­flow X.”

I also pic­ture the Snif­fer as being hid­den entirely within the wrap­per. The wrap­per (or the Lar­avel Ser­vice Provider) instan­ti­ates the Snif­fer, attaches it to the work­flow exe­cu­tion, and han­dles any queries of the Sniffer.

We’ll need var­i­ous tests ensur­ing that this mech­a­nism actu­ally works, for the main work­flow, sub work­flows, and par­al­lel executions.

Trac­ing

As an added bonus, this might be a good mech­a­nism for imple­ment­ing a work­flow event trace. How­ever, an exe­cu­tion visu­al­izer plu­gin already exists, doc­u­mented at http://ezcomponents.org/docs/api/latest/Workflow/ezcWorkflowExecutionVisualizerPlugin.html.

That being the case, I’ll set aside the trac­ing idea for now. If I were to cre­ate a trac­ing mech­a­nism, it looks like cre­at­ing a sep­a­rate plu­gin for that pur­pose is the way to go. That keeps the plu­gin in line with the Sin­gle Respon­si­b­lity Prin­ci­ple, and we only need to attach it to a work­flow when trac­ing is needed.

Sin­gle Responsibility

The work­flow can run a list of any num­ber of plu­g­ins. Each plu­gin can attach itself to any num­ber of events, or focus on a sin­gle spe­cific event. For exam­ple, a plu­gin can be acti­vated when the work­flow sus­pends and when the work­flow ends, but ignore all other events.

That being the case, it makes sense, when imple­ment­ing plu­g­ins, to make a num­ber of small plu­g­ins with a sin­gle clear respon­si­bil­ity or pur­pose, rather than a larger plu­gin with mul­ti­ple responsibilities.

If we come up with a bunch of plu­g­ins, we might even want to cre­ate a plu­gin that attaches the plugins!

Snif­fer Components

As a first cut, we can sep­a­rate the Snif­fer into the fol­low­ing com­po­nents. Each com­po­nent has a sin­gle well defined responsibility.

  • The plu­gin itself extends ezc­Work­flowEx­e­cu­tion­Plu­gin. The plu­gin com­mu­ni­cates with the execution.
  • We main­tain exe­cu­tion state with an Exe­cu­tion State object. Given the Zeta Work­flow exe­cu­tion, the Exe­cu­tion State object cap­tures and retains that execution’s cur­rent exe­cu­tion state. Our Snif­fer will have mul­ti­ple Exe­cu­tion State objects.
  • The Exe­cu­tion State Inter­preter is closely tied to what­ever mes­sage a node meant to send. For exam­ple, the Exe­cu­tion State Inter­preter might pass back the infor­ma­tion that we are now to switch to work­flow X. This for­mal­izes how we already announce the next slide to dis­play. We check the exe­cu­tion vari­able ‘slide’.
  • The Exe­cu­tion Snif­fer com­mu­ni­cates with the wrap­per. The Exe­cu­tion Snif­fer is itself a com­po­si­tion bring­ing together the above components.

Since we have sev­eral com­po­nents, we’ll cre­ate a new Snif­fer direc­tory and a cor­re­spond­ing tests/sniffer direc­tory. We’ll define respec­tive file name­spaces accordingly.

Fea­ture Details

Let’s lay out what each com­po­nent should do. We’ll quickly dis­cover that we’ll need another com­po­nent which keeps track of the var­i­ous Exe­cu­tion State objects. Call it the Exe­cu­tion State Registry.

The Plu­gin

The avail­able meth­ods are doc­u­mented here: http://ezcomponents.org/docs/api/latest/Workflow/ezcWorkflowExecutionPlugin.html. For now let’s keep this sim­ple and only imple­ment afterNodeExecuted().

When called, we want to do the following.

  • Notify the Exe­cu­tion State Reg­istry that this exe­cu­tion is, as of this moment, the cur­rent execution.
  • Obtain the rel­e­vant Exe­cu­tion State object from the Exe­cu­tion State Registry.
  • Have the rel­e­vant Exe­cu­tion State object cap­ture and retain this execution’s cur­rent exe­cu­tion state.

Our unit test, there­fore, can be fright­fully sim­ple. We can pass in mocks of the exe­cu­tion and node, and con­di­tion mocks of the Exe­cu­tion State Reg­istry and Exe­cu­tion State object to ensure they are called as intended.

To be sure, we may have some fright­fully com­plex sce­nar­ios to explore, such as syn­chro­nized par­al­lel exe­cu­tions with paused sub work­flows. But we’ll get to those later!

The Exe­cu­tion State Object

The exe­cu­tion state object retains the exe­cu­tion state as defined by isCan­celled(), hasEnded(), and isSus­pended(); and the exe­cu­tion vari­ables as obtained via getVariables().

The func­tion­al­ity is:

  • Cap­ture exe­cu­tion state, given the exe­cu­tion as passed to the plugin.
  • Dis­gorge the last-captured exe­cu­tion state. Mir­ror the exe­cu­tion meth­ods isCan­celled(), hasEnded(), isSus­pended, get­Vari­ables(), get­Vari­able(), has­Vari­able(), getId().

Our unit tests will explore the API of each method, fol­lowed by ver­i­fy­ing the func­tion­al­ity in each case.

The Exe­cu­tion State Interpreter

We’ll keep the inter­preter sim­ple. 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.

We could do a series of work­flow swaps, for exam­ple, if our first work­flow deter­mines the customer’s demo­graph­ics; we swap to a sec­ond work­flow which deter­mines the appro­pri­ate offer group; that offer group is a work­flow which presents a spe­cific offer.

Where this gets tricky is with sub work­flows and par­al­lel exe­cu­tions. So far as I know, if we are switch­ing to a new work­flow, the only thing we can do is can­cel the cur­rent work­flow, and pro­ceed with the new work­flow. The can­cel() destroys all ves­tiges of the cur­rent work­flow, includ­ing sub work­flows and par­al­lel threads.

Another option might be to leave the cur­rent work­flow sus­pended, run the new work­flow, and then resume the sus­pended workflow.

Another option might be that the work­flow ends itself, pre­sent­ing the new work­flow name as its final result. We then pro­ceed with the new workflow.

The Exe­cu­tion Sniffer

The Exe­cu­tion Snif­fer com­mu­ni­cates with the Zeta Com­po­nents Inter­face (Wrap­per). Either the Wrap­per, or the Snif­fer, is respon­si­ble for ensur­ing the plu­gin is attached to all workflows.

Since the Wrap­per is respon­si­ble for instan­ti­at­ing work­flow exe­cu­tions, I’m inclined to make the Wrap­per respon­si­ble for attach­ing the plu­gin when it does so. But, as I think about that, I think we’ll make the Wrap­per respon­si­ble for telling the Snif­fer to attach its plugin.

We need to do a bit of home­work. Once a plu­gin is attached to the main work­flow, does it get attached to sub work­flows and par­al­lel threads?

Let’s check the sub work­flow 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 work­flows if we are using the tied data­base. def­i­n­i­tion­Stor­age is the tied data­base com­po­nent, and line three makes it manda­tory for the Sub Work­flow node.

Get­SubEx­e­cu­tion() is in zetacomponents/workflow/src/interfaces/execution.php, the same file con­tain­ing start() and resume() we stud­ied 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 plu­gin in the par­ent exe­cu­tion (line 29), we add that plu­gin to the new sub exe­cu­tion (line 31).

What about par­al­lel exe­cu­tions? I took a moment to chase that down as best I could from the online doc­u­men­ta­tion, and it looks like we acti­vate nodes in the cur­rent exe­cu­tion but do NOT cre­ate a new exe­cu­tion. So, it appears we have our plu­gin in place. If we ever do par­al­lel threads, we’ll want to test and val­i­date our assumption.

Home­work com­plete, let’s lay out the Exe­cu­tion Snif­fer features.

  • When requested by the wrap­per, and given the exe­cu­tion, attach our plu­gin to the exe­cu­tion. attachPlugin()
  • Return the cur­rent Exe­cu­tion State Inter­preter result. currentInterpretation()
  • Return exe­cu­tion state of the cur­rent Exe­cu­tion State object. cur­ren­tIs­Can­celled(), cur­ren­tHasEnded(), cur­ren­tIs­Sus­pended(), cur­rent­Get­Vari­ables(), cur­rent­Get­Vari­able(), cur­ren­tHas­Vari­able(), currentGetId().

We’ll have the Lar­avel Ser­vice Provider wire every­thing together.

Exe­cu­tion State Registry

Since we poten­tially have mul­ti­ple exe­cu­tions, we need a reg­istry to man­age the mul­ti­ple Exe­cu­tion State objects. The reg­istry also main­tains the notion of which is the “cur­rent” Exe­cu­tion State object. The fea­tures are:

  • Accept noti­fi­ca­tion that “this” is the cur­rent execution.
  • For a given exe­cu­tion, return the rel­e­vant Exe­cu­tion State object. We either fetch it from our cache, or con­struct it on the spot. I am assum­ing that, by this point, each exe­cu­tion has an id. If not, ini­tially, throw an excep­tion. Later, we should prob­a­bly ignore exe­cu­tions with no id, assum­ing that later in the work­flow exe­cu­tion it WILL have an id, and we can snag it at that point.
  • Return the “cur­rent” Exe­cu­tion State object.

Devel­op­ment Plan

Our gen­eral approach is to begin with the com­po­nent with the least depen­den­cies. We begin by writ­ing tests explor­ing its inter­face (con­struc­tor and callable meth­ods). We then drill down, flesh­ing out and ver­i­fy­ing its functionality.

Hav­ing fully devel­oped one com­po­nent, we can accu­rately mock it while devel­op­ing later com­po­nents. Hav­ing fully unit tested it, we can be con­fi­dent that mock­ing is sufficient.

We’ll develop our com­po­nents in the fol­low­ing order, based on each component’s dependencies:

  1. The Exe­cu­tion State Object. Its only depen­dency is the exe­cu­tion, which we’ll mock.
  2. The Exe­cu­tion State Reg­istry. Its only depen­den­cies are Exe­cu­tion State Objects.
  3. The Exe­cu­tion State Inter­preter. It acts on an Exe­cu­tion State Object.
  4. The Plu­gin. It works with the Exe­cu­tion State Reg­istry and Exe­cu­tion State Objects.
  5. The Exe­cu­tion Snif­fer. It is com­posed of each of the above objects.

Up next: The Exe­cu­tion 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 Zeta­Com­po­nentsWrap­per is a Com­po­si­tion bring­ing together mul­ti­ple objects needed for using the Zeta Com­po­nents Workflow:

  • $this->db, defined in the par­ent class. This the Tied Data­base con­nec­tion. The Zeta Com­po­nents Work­flow Data­base Tie-in pack­age makes a sep­a­rate con­nec­tion to our Lar­avel data­base, using the Zeta Com­po­nents Data­base package.
  • $this->workflow. This the Zeta Com­po­nents Work­flow object as defined in the Zeta Com­po­nents Work­flow package.
  • $this->definition. This is the tied-database ver­sion of the Zeta Com­po­nents Work­flow def­i­n­i­tion. It is used to load and store the work­flow object to/from the tied database.
  • $this->xmldefinition (not imple­mented). This is the xml-file ver­sion of the Zeta Com­po­nents Work­flow def­i­n­i­tion. It is used to load and store the work­flow object to/from XML files.
  • $this->runner. An exe­cu­tion of the cur­rent work­flow. This is the “inter­ac­tive” exe­cu­tion, and is asso­ci­ated with the tied database.
  • $this->slide. A helper class that maps our slide names to their asso­ci­ated Work­flow Action Nodes. This class sim­pli­fies the com­plex­ity of say­ing, “At this point in the flow I want to present this slide to the user.”
  • $this->node. A helper class that manip­u­lates and builds up Work­flow Nodes. This class sim­pli­fies var­i­ous Work­flow Node Com­po­si­tion 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 Com­po­nents pack­age from Lar­avel, you will be using the Zeta Com­po­nents Wrap­per. In PHP (and Lar­avel) terms, the Zeta Com­po­nents Wrap­per imple­ments the Zeta Com­po­nents Inter­face spec­i­fi­ca­tion. The rest of this post is a copy of the PHP­Doc­u­men­tor doc­u­men­ta­tion inside the Zeta Com­po­nents Interface.

  • The Zeta Com­po­nents Inter­face 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 php­doc 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 Work­flow code does not cor­rectly han­dle excep­tions. This walk­through describes the behav­ior as mod­i­fied by myself.

The basic issue was that both start() and resume(), when using the Data­base Tie-In com­po­nent, issue begin­Trans­ac­tion() to the data­base engine. The work­flows can throw excep­tions which remain uncaught. Because the excep­tion is not caught within the work­flow code, the trans­ac­tion is never closed with either roll­back or com­mit. When using an sqlite data­base, unit tests then fail, com­plain­ing of a locked table.

Within start() and resume(), we now catch the excep­tions so that we can com­mit or roll back the trans­ac­tion before (pos­si­bly) 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:

Next Page »