Adamw has uploaded a new change for review. https://gerrit.wikimedia.org/r/122220
Change subject: WIP Standard libraries; integration tests ...................................................................... WIP Standard libraries; integration tests Change-Id: I98114dbdeeac5a12a50ef6252120d12fa2ee34bd --- A includes/library/Alarm.php A includes/library/MachineHierarchy.php A includes/library/SelfStimulating.php A includes/library/TaggedPage.php A includes/library/WikiPages.php A tests/BaseWorkflowTestCase.php A tests/DoorLibrary.php A tests/TestStateMachine.php A tests/TestStateMachineDescription.php A tests/ennui_machine.yaml 10 files changed, 474 insertions(+), 0 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Workflow refs/changes/20/122220/1 diff --git a/includes/library/Alarm.php b/includes/library/Alarm.php new file mode 100644 index 0000000..f4e5177 --- /dev/null +++ b/includes/library/Alarm.php @@ -0,0 +1,86 @@ +<?php namespace Workflow\Library; + +use \Job; + +/** + * Simple deferred self-signaling + */ +class Alarm + implements IMachineLibrary +{ + protected $actions; + + function __construct() { + $this->actions = array( + 'alarm' => new AlarmAction(), + ); + } + + function getActions() { + return $this->actions; + } +} + +class AlarmAction + implements ITransactionalAction +{ + protected $job; + + function begin() {} + + /** + * @param array $params ( + * 'signal' => string signal name + * 'interval' => string relative time interval + * ) + */ + function run( IStateMachine $machine, $params ) { + global $wgJobClasses; + + list ( $signal, $interval ) = $params; + + $scheduledTime = strtotime( $interval ); + // TODO: cannot schedule job time yet. See https://bugzilla.wikimedia.org/show_bug.cgi?id=60217 + + $this->job = new AlarmJob( null, array( + // FIXME: I suspect there are some limitations on $params + // serializability; we might need to pass job_id here rather than + // $machine. + 'machine' => $machine, + 'signal' => $signal, + ) ); + + $wgJobClasses[$this->job->getCommand()] = get_class( $this->job ); + JobQueueGroup::singleton()->push( $this->job ); + } + + function commit() {} + + function rollback() { + // TODO: detect when we are too late to prevent the job, and... do something. + JobQueueGroup::singleton()->ack( $this->job ); + } +} + +class AlarmJob + extends Job +{ + static protected $command = 'workflowAlarmJob'; + + protected $machine; + protected $signal; + + function getCommand() { + return static::$command; + } + + function __construct( $title, $params ) { + parent::__construct( static::$command, $title, $params ); + $this->machine = $params['machine']; + $this->signal = $params['signal']; + } + + function run() { + $this->machine->signal( $this->signal ); + } +} diff --git a/includes/library/MachineHierarchy.php b/includes/library/MachineHierarchy.php new file mode 100644 index 0000000..acac4bc --- /dev/null +++ b/includes/library/MachineHierarchy.php @@ -0,0 +1,16 @@ +<?php namespace Workflow\Library; + +/** + * Supports subtasks which are themselves a state machine + */ +class MachineHierarchy + implements IMachineLibrary +{ + protected $actions = array( + 'submachine', + ); + + function handle_submachine( IStateMachine $machine, $params ) { + // TODO design. message passing. + } +} diff --git a/includes/library/SelfStimulating.php b/includes/library/SelfStimulating.php new file mode 100644 index 0000000..e5f0359 --- /dev/null +++ b/includes/library/SelfStimulating.php @@ -0,0 +1,50 @@ +<?php namespace Workflow\Library; + +use Workflow\IStateMachine; + +/** + * Use this mixin to implement a machine which can send itself signals. Without + * this, control flow is always relinquished after entering a state. + */ +class SelfStimulating + implements IMachineLibrary +{ + protected $actions; + + function __construct() { + $this->actions = array( + 'signal' => new SignalAction(), + ); + } + + function getActions() { + return $this->actions; + } +} + +class SignalAction + implements ITransactionalAction +{ + protected $machine; + protected $signal; + + function beginTransaction() {} + + /** + * @param array $params ( + * string $signal - name of the signal to send + * ); + */ + function run( IStateMachine $machine, $params ) { + $this->machine = $machine; + + // parse params + list( $this->signal ) = $params; + } + + function commitTransaction() { + $this->machine->signal( $this->signal ); + } + + function rollbackTransaction() {} +} diff --git a/includes/library/TaggedPage.php b/includes/library/TaggedPage.php new file mode 100644 index 0000000..db8817f --- /dev/null +++ b/includes/library/TaggedPage.php @@ -0,0 +1,52 @@ +<?php namespace Workflow\Library; + +use Workflow\IStateMachine; + +/** + * Utilities to tag pages with a category, or assert that a page is or is not already tagged with a category + */ +class TaggedPage + implements IMachineLibrary +{ + protected $actions; + + function __construct() { + $this->actions = array( + 'tag' => new TagAction(), + 'untag' => new UntagAction(), + 'assert_tagged' => new AssertTaggedAction(), + 'assert_untagged' => new AssertUntaggedAction(), + ); + } + + function getActions() { + return $this->actions; + } +} + +class TagAction + implements ITransactionalAction +{ + protected $machine; + protected $signal; + + function begin() {} + + /** + * @param array $params ( + * string $signal - name of the signal to send + * ); + */ + function run( IStateMachine $machine, $params ) { + $this->machine = $machine; + + // parse params + list( $this->signal ) = $params; + } + + function commit() { + $this->machine->signal( $this->signal ); + } + + function rollback() {} +} diff --git a/includes/library/WikiPages.php b/includes/library/WikiPages.php new file mode 100644 index 0000000..0ef220e --- /dev/null +++ b/includes/library/WikiPages.php @@ -0,0 +1,42 @@ +<?php namespace Workflow\Library; + +use Workflow\Exception\InvalidActionException; + +/** + * Support UI flow elements + */ +class WikiPages + implements IMachineLibrary +{ + function getActions() { + return array( + 'redirect', + ); + } + + function handleAction( IStateMachine $machine, $action, $params ) { + switch ( $action ) { + case 'redirect': + $this->handle_redirect( $machine, $params ); + break; + default: + throw new InvalidActionException( $action ); + } + } + + /** + * @param array $params ( + * string $titleVar - name of the configuration variable holding the target page title + * ); + * + * TODO: whitelist page review status + */ + function handle_redirect( $machine, $params ) { + list( $titleVar ) = $params; + + $output = RequestContext::getMain()->getOutput(); + $key = $machine->getValue( $titleVar ); + $url = Title::newFromText( $key )->getFullURL(); + $output->redirect( $url ); + } +} diff --git a/tests/BaseWorkflowTestCase.php b/tests/BaseWorkflowTestCase.php new file mode 100644 index 0000000..07eb9ef --- /dev/null +++ b/tests/BaseWorkflowTestCase.php @@ -0,0 +1,20 @@ +<?php namespace Workflow\Tests; + +use \MediaWikiTestCase; + +use Workflow\Description\DescriptionLoader; +use Workflow\StateMachine; + +class BaseWorkflowTestCase extends MediaWikiTestCase { + protected $description; + protected $machine; + + protected function setUp() { + parent::setUp(); + + $descriptionPath = __DIR__ . '/ennui_machine.yaml'; + + $this->description = DescriptionLoader::loadFromFile( $descriptionPath ); + $this->machine = new StateMachine( $this->description ); + } +} diff --git a/tests/DoorLibrary.php b/tests/DoorLibrary.php new file mode 100644 index 0000000..3d21d77 --- /dev/null +++ b/tests/DoorLibrary.php @@ -0,0 +1,51 @@ +<?php namespace Workflow\Tests; + +use Workflow\IStateMachine; +use Workflow\Library\IAction; +use Workflow\Library\IMachineLibrary; +use Workflow\Exception\TransitionAbortedException; + +class DoorLibrary + implements IMachineLibrary +{ + const DOOR_VAR = 'door_locked'; + + function getActions() { + return array( + 'lock' => new LockAction(), + 'unlock' => new UnlockAction(), + 'assert_unlocked' => new AssertUnlockedAction(), + ); + } +} + +class LockAction + implements IAction +{ + function run( IStateMachine $machine, $params ) { + $machine->setValue( DoorLibrary::DOOR_VAR, true ); + } +} + +class UnlockAction + implements IAction +{ + function run( IStateMachine $machine, $params ) { + $machine->setValue( DoorLibrary::DOOR_VAR, false ); + } +} + +class AssertUnlockedAction + implements IAction +{ + function run( IStateMachine $machine, $params ) { + if ( $machine->getValue( DoorLibrary::DOOR_VAR ) ) { + throw new LockedException(); + } + } +} + +class LockedException + extends TransitionAbortedException +{ +} diff --git a/tests/TestStateMachine.php b/tests/TestStateMachine.php new file mode 100644 index 0000000..d247091 --- /dev/null +++ b/tests/TestStateMachine.php @@ -0,0 +1,73 @@ +<?php namespace Workflow\Tests; + +/** + * @group Workflow + */ +class TestStateMachine extends BaseWorkflowTestCase { + + protected function setUp() { + parent::setUp(); + + $this->machine->start(); + } + + function testInitialState() { + $this->assertSame( 'inside', $this->machine->getState() ); + } + + function testTransition() { + $this->machine->signal( 'walk' ); + + $this->assertSame( 'at_threshold', $this->machine->getState() ); + } + + /** + * @expectedException Workflow\Exception\InvalidTransitionException + */ + function testInvalidTransition() { + $this->machine->signal( 'bogon' ); + } + + function testExceptionSignal() { + $this->machine->signal( 'give_up' ); + + $this->assertSame( 'trapped', $this->machine->getState() ); + } + + function testConfiguration() { + $this->assertSame( false, $this->machine->getValue( 'door_locked' ) ); + } + + function testTransitionAction() { + $this->machine->signal( 'walk' ); + $this->machine->signal( 'use_key' ); + + $this->assertSame( true, $this->machine->getValue( 'door_locked' ) ); + } + + function testStateAction() { + $this->machine->signal( 'walk' ); + $this->machine->signal( 'open_door' ); + $this->machine->signal( 'exit' ); + + $this->assertSame( 'gone', $this->machine->getState() ); + } + + function testCompletion() { + $this->machine->signal( 'walk' ); + $this->machine->signal( 'open_door' ); + $this->machine->signal( 'exit' ); + + $this->assertTrue( $this->machine->isFinished() ); + } + + function testRollback() { + $this->machine->signal( 'walk' ); + $this->machine->signal( 'use_key' ); + + // Locked door will prevent the transition + $this->machine->signal( 'open_door' ); + + $this->assertSame( 'at_threshold', $this->machine->getState() ); + } +} diff --git a/tests/TestStateMachineDescription.php b/tests/TestStateMachineDescription.php new file mode 100644 index 0000000..3609cdb --- /dev/null +++ b/tests/TestStateMachineDescription.php @@ -0,0 +1,47 @@ +<?php namespace Workflow\Tests; + +/** + * @group Workflow + */ +class TestStateMachineDescription extends BaseWorkflowTestCase { + + function testLoadYamlDescription() { + $this->assertNotNull( $this->description ); + } + + function testInitialState() { + $this->assertEquals( 'inside', $this->description->getInitialState() ); + } + + function testGetTransitionsForState() { + $transitions = $this->description->getTransitionsForState( 'inside' ); + sort( $transitions ); + $this->assertSame( array( 'fidget', 'walk' ), $transitions ); + } + + function testGetDestinationState() { + $this->assertSame( 'at_threshold', $this->description->getTransition( 'inside', 'walk' )->getDestinationName() ); + } + + function testGetExceptions() { + $this->assertSame( array( 'give_up' ), $this->description->getExceptions() ); + } + + function testGetActionsForTransition() { + $actionInvocations = $this->description->getTransition( 'at_threshold', 'use_key' )->getActionInvocations(); + $this->assertSame( array( 'lock' ), array_keys( $actionInvocations ) ); + $this->assertSame( null, $actionInvocations['lock']->getParams() ); + } + + function testGetActionsForState() { + $this->assertSame( array(), $this->description->getActionsForState( 'at_threshold' ) ); + + $actionInvocations = $this->description->getActionsForState( 'at_liberty' ); + $this->assertSame( array( 'signal' ), array_keys( $actionInvocations ) ); + $this->assertSame( 'exit', $actionInvocations['signal']->getParams() ); + } + + function testGetConfigurationValue() { + $this->assertSame( false, $this->description->getConfigurationValue( 'door_locked' ) ); + } +} diff --git a/tests/ennui_machine.yaml b/tests/ennui_machine.yaml new file mode 100644 index 0000000..b8c774c --- /dev/null +++ b/tests/ennui_machine.yaml @@ -0,0 +1,37 @@ +# Common-sensical demonstration FSM +title: Test Machine + +# FIXME: these PHP namespaces jump out like a second thumb, oh well. +libraries: + - Workflow\Library\SelfStimulating + - Workflow\Tests\DoorLibrary + +states: + - inside: + transitions: + walk: at_threshold + fidget: inside + - at_threshold: + transitions: + open_door: + actions: + - assert_unlocked: + destination: at_liberty + return: inside + use_key: + actions: + - lock: + destination: at_threshold + - at_liberty: + actions: + - signal: exit + transitions: + exit: gone + - gone: + - trapped: + +exceptions: + give_up: trapped + +configuration: + door_locked: false -- To view, visit https://gerrit.wikimedia.org/r/122220 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I98114dbdeeac5a12a50ef6252120d12fa2ee34bd Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/Workflow Gerrit-Branch: master Gerrit-Owner: Adamw <awi...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits