Adamw has uploaded a new change for review.
https://gerrit.wikimedia.org/r/107307
Change subject: Basic state machinery
......................................................................
Basic state machinery
TODO:
* Logging
* Tease out transition actions vs enterstate actions, pre and post-signal hooks
* Design UI flow interface (library/WikiPages.php)
* Machine ID and Workflow Instance ID; address signal to known job
* Suspend machine & persist state
* Provide full examples, or even better, verbose integration tests
* Remote API and token
* ContentHandler for the Workflow: namespace
syntax validation
Change-Id: I3f74158289f19efe15546bbacde79b40afed14c2
---
A Workflow.hooks.php
A Workflow.i18n.php
A Workflow.php
A includes/BusyMachineException.php
A includes/Context.php
A includes/IStateMachine.php
A includes/InvalidActionException.php
A includes/InvalidLibraryException.php
A includes/InvalidStateException.php
A includes/InvalidTransitionException.php
A includes/StateMachine.php
A includes/StateMachineDescription.php
A includes/TransitionNotPossibleException.php
A includes/library/BaseMachineLibrary.php
A includes/library/IMachineLibrary.php
A includes/library/MachineHierarchy.php
A includes/library/SelfStimulating.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
23 files changed, 835 insertions(+), 0 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Workflow
refs/changes/07/107307/1
diff --git a/Workflow.hooks.php b/Workflow.hooks.php
new file mode 100644
index 0000000..19713b7
--- /dev/null
+++ b/Workflow.hooks.php
@@ -0,0 +1,11 @@
+<?php namespace Workflow;
+
+class Hooks {
+ static function onUnitTestsList( array& $files ) {
+ $dir = __DIR__ . '/tests';
+ $files[] = $dir . '/TestStateMachine.php';
+ $files[] = $dir . '/TestStateMachineDescription.php';
+
+ return true;
+ }
+}
diff --git a/Workflow.i18n.php b/Workflow.i18n.php
new file mode 100644
index 0000000..c6eac29
--- /dev/null
+++ b/Workflow.i18n.php
@@ -0,0 +1,28 @@
+<?php
+
+$messages = array();
+
+$messages['en'] = array(
+ 'workflow-desc' => 'Provides process management',
+
+ 'workflow-busy-error' => 'Attempted to operate on a busy machine',
+ 'workflow-invalid-action' => 'Invalid action "$1"',
+ 'workflow-invalid-library' => 'Invalid library "$1"',
+ 'workflow-invalid-state' => 'Invalid state name "$1"',
+ 'workflow-invalid-transition' => 'No such transition "$2" out of state
"$1"',
+);
+
+$messages['qqq'] = array(
+ 'workflow-desc' =>
'{{desc|name=Workflow|url=http://www.mediawiki.org/wiki/Extension:Workflow}}',
+
+ 'workflow-busy-error' => 'Error message thrown when a busy machine
receives a signal or serialization is attempted.',
+ 'workflow-invalid-action' => 'Error message thrown when an invalid
action is attempted. Parameters:
+$1 - action name',
+ 'workflow-invalid-library' => 'Error message thrown if an invalid
library load is attempted. Parameters:
+$1 - library name',
+ 'workflow-invalid-state' => 'Error message thrown when an invalid state
name is detected. Parameters:
+$1 - state name',
+ 'workflow-invalid-transition' => 'Error message thrown when an invalid
transition is attempted. Parameters:
+$1 - state name
+$2 - transition name',
+);
diff --git a/Workflow.php b/Workflow.php
new file mode 100644
index 0000000..d0557e5
--- /dev/null
+++ b/Workflow.php
@@ -0,0 +1,57 @@
+<?php
+
+$wgExtensionCredits['other'][] = array(
+ 'path' => __FILE__,
+ 'name' => 'Workflow',
+ 'author' => array(
+ 'Adam Roses Wight',
+ ),
+ 'version' => '0.1',
+ 'url' => 'https://www.mediawiki.org/wiki/Extension:Workflow',
+ 'descriptionmsg' => 'workflow-desc',
+);
+
+$dir = __DIR__;
+
+$wgExtensionMessagesFiles['Workflow'] = $dir . '/Workflow.i18n.php';
+
+$wgAutoloadClasses['Workflow\Hooks'] = $dir . '/Workflow.hooks.php';
+
+$includedir = __DIR__ . '/includes/';
+$wgAutoloadClasses['Workflow\BusyMachineException'] = $includedir .
'BusyMachineException.php';
+$wgAutoloadClasses['Workflow\Context'] = $includedir . 'Context.php';
+$wgAutoloadClasses['Workflow\InvalidActionException'] = $includedir .
'InvalidActionException.php';
+$wgAutoloadClasses['Workflow\InvalidLibraryException'] = $includedir .
'InvalidLibraryException.php';
+$wgAutoloadClasses['Workflow\InvalidStateException'] = $includedir .
'InvalidStateException.php';
+$wgAutoloadClasses['Workflow\InvalidTransitionException'] = $includedir .
'InvalidTransitionException.php';
+$wgAutoloadClasses['Workflow\IStateMachine'] = $includedir .
'IStateMachine.php';
+$wgAutoloadClasses['Workflow\StateMachine'] = $includedir . 'StateMachine.php';
+$wgAutoloadClasses['Workflow\StateMachineDescription'] = $includedir .
'StateMachineDescription.php';
+$wgAutoloadClasses['Workflow\TransitionNotPossibleException'] = $includedir .
'TransitionNotPossibleException.php';
+
+$librarydir = __DIR__ . '/includes/library/';
+$wgAutoloadClasses['Workflow\Library\BaseMachineLibrary'] = $librarydir .
'BaseMachineLibrary.php';
+$wgAutoloadClasses['Workflow\Library\IMachineLibrary'] = $librarydir .
'IMachineLibrary.php';
+$wgAutoloadClasses['Workflow\Library\MachineHierarchy'] = $librarydir .
'MachineHierarchy.php';
+$wgAutoloadClasses['Workflow\Library\SelfStimulating'] = $librarydir .
'SelfStimulating.php';
+$wgAutoloadClasses['Workflow\Library\WikiPages'] = $librarydir .
'WikiPages.php';
+
+# FIXME: isn't there another variable only used during testing?
+$testdir = __DIR__ . '/tests/';
+$wgAutoloadClasses['Workflow\Tests\BaseWorkflowTestCase'] = $testdir .
'BaseWorkflowTestCase.php';
+$wgAutoloadClasses['Workflow\Tests\DoorLibrary'] = $testdir .
'DoorLibrary.php';
+
+define( 'NS_WORKFLOW', 204 );
+define( 'NS_WORKFLOW_TALK', 205 );
+
+$wgExtraNamespaces['NS_WORKFLOW'] = 'Workflow';
+$wgExtraNamespaces['NS_WORKFLOW_TALK'] = 'Workflow_talk';
+
+# TODO
+# $wgNamespaceContentModels[NS_WORKFLOW] = 'StateMachineDescription';
+# $wgContentHandlers['StateMachineDescription'] =
'StateMachineDescriptionContentHandler';
+
+$wgHooks['UnitTestsList'][] = 'Workflow\Hooks::onUnitTestsList';
+
+# FIXME: move to a setup hook
+require_once __DIR__ . '/vendor/autoload.php';
diff --git a/includes/BusyMachineException.php
b/includes/BusyMachineException.php
new file mode 100644
index 0000000..034f1f1
--- /dev/null
+++ b/includes/BusyMachineException.php
@@ -0,0 +1,11 @@
+<?php namespace Workflow;
+
+use \Exception;
+
+class BusyMachineException extends Exception {
+ function __construct() {
+ parent::__construct();
+
+ $this->message = wfMessage( 'workflow-busy-error' )->text();
+ }
+}
diff --git a/includes/Context.php b/includes/Context.php
new file mode 100644
index 0000000..0df0937
--- /dev/null
+++ b/includes/Context.php
@@ -0,0 +1,39 @@
+<?php namespace Workflow;
+
+use \Serializable;
+use \JsonSerializable;
+
+/**
+ * State data for a workflow instance
+ *
+ * TODO: contain metadata about the job?
+ */
+class Context
+ implements Serializable, JsonSerializable
+{
+ protected $data = array();
+
+ function getValue( $key ) {
+ if ( array_key_exists( $key, $this->data ) ) {
+ return $this->data[$key];
+ }
+ return null;
+ }
+
+ function setValue( $key, $value ) {
+ $this->data[$key] = $value;
+ }
+
+ function serialize() {
+ return $this->data;
+ // TODO: freeze self
+ }
+
+ function jsonSerialize() {
+ return $this->serialize();
+ }
+
+ function unserialize( $data ) {
+ $this->data = $data;
+ }
+}
diff --git a/includes/IStateMachine.php b/includes/IStateMachine.php
new file mode 100644
index 0000000..044855e
--- /dev/null
+++ b/includes/IStateMachine.php
@@ -0,0 +1,29 @@
+<?php namespace Workflow;
+
+/**
+ *
+ */
+interface IStateMachine {
+ /**
+ * Start the machine in its initial state. Run any actions on that
state.
+ */
+ function begin();
+
+ /**
+ * Send this object a signal, causing a transition to begin
+ */
+ function signal( $name );
+
+ /**
+ * Doing something atomic?
+ *
+ * @return bool true if there is a transition in progress, or the
machine
+ * is being serialized. I pity the foo who messes with a busy job.
+ */
+ function isBusy();
+
+ /**
+ * @return bool true when there are no transitions out of the current
state
+ */
+ function isFinished();
+}
diff --git a/includes/InvalidActionException.php
b/includes/InvalidActionException.php
new file mode 100644
index 0000000..be4d46a
--- /dev/null
+++ b/includes/InvalidActionException.php
@@ -0,0 +1,11 @@
+<?php namespace Workflow;
+
+use \Exception;
+
+class InvalidActionException extends Exception {
+ function __construct( $action ) {
+ parent::__construct();
+
+ $this->message = wfMessage( 'workflow-invalid-action', $action
)->text();
+ }
+}
diff --git a/includes/InvalidLibraryException.php
b/includes/InvalidLibraryException.php
new file mode 100644
index 0000000..3b29fc1
--- /dev/null
+++ b/includes/InvalidLibraryException.php
@@ -0,0 +1,11 @@
+<?php namespace Workflow;
+
+use \Exception;
+
+class InvalidLibraryException extends Exception {
+ function __construct( $library ) {
+ parent::__construct();
+
+ $this->message = wfMessage( 'workflow-library-action', $library
)->text();
+ }
+}
diff --git a/includes/InvalidStateException.php
b/includes/InvalidStateException.php
new file mode 100644
index 0000000..80c809f
--- /dev/null
+++ b/includes/InvalidStateException.php
@@ -0,0 +1,11 @@
+<?php namespace Workflow;
+
+use \Exception;
+
+class InvalidStateException extends Exception {
+ function __construct( $state ) {
+ parent::__construct();
+
+ $this->message = wfMessage( 'workflow-invalid-state', $state
)->text();
+ }
+}
diff --git a/includes/InvalidTransitionException.php
b/includes/InvalidTransitionException.php
new file mode 100644
index 0000000..cd55887
--- /dev/null
+++ b/includes/InvalidTransitionException.php
@@ -0,0 +1,11 @@
+<?php namespace Workflow;
+
+use \Exception;
+
+class InvalidTransitionException extends Exception {
+ function __construct( $state, $transition ) {
+ parent::__construct();
+
+ $this->message = wfMessage( 'workflow-invalid-transition',
$state, $transition )->text();
+ }
+}
diff --git a/includes/StateMachine.php b/includes/StateMachine.php
new file mode 100644
index 0000000..d192474
--- /dev/null
+++ b/includes/StateMachine.php
@@ -0,0 +1,181 @@
+<?php namespace Workflow;
+
+use \Exception;
+use Workflow\Library\IMachineLibrary;
+
+/**
+ * Basic FSM engine supporting exceptions and preconditions
+ *
+ * An exception is just a shortcut notation for a transition exiting from
every state.
+ */
+class StateMachine
+ implements IStateMachine
+{
+ protected $state;
+
+ protected $busy = false;
+ protected $nextSignal;
+
+ protected $context;
+
+ protected $description;
+
+ protected $libraries = array();
+
+ function __construct( StateMachineDescription $desc ) {
+ $this->description = $desc;
+ $this->context = new Context();
+
+ foreach ( $this->description->getLibraries() as $libraryName ) {
+ $obj = new $libraryName();
+ if ( !( $obj instanceof IMachineLibrary ) ) {
+ throw new InvalidLibraryException( $libraryName
);
+ }
+ $this->libraries[] = $obj;
+ }
+ }
+
+ function begin() {
+ $initialState = $this->description->getInitialState();
+ $this->enterState( $initialState );
+ }
+
+ function signal( $name ) {
+ if ( $this->isBusy() ) {
+ // We only tolerate a one-signal queue, which we expect
will be
+ // potentially generated by an enterState action.
+ $this->enqueueSignal( $name );
+ return;
+ }
+
+ do {
+ $this->doTransition( $name );
+
+ // Catch up with signals that might have been generated
during the transition
+ $name = $this->nextSignal;
+ $this->nextSignal = null;
+ } while ( $name !== null );
+ }
+
+ function enqueueSignal( $name ) {
+ if ( $this->nextSignal ) {
+ // FIXME: new type of exception, "stack full" or
something
+ throw new BusyMachineException();
+ }
+ $this->nextSignal = $name;
+ }
+
+ protected function doTransition( $transition ) {
+ if ( in_array( $transition, $this->description->getExceptions()
) ) {
+ $destinationState =
$this->description->getExceptionDestination( $transition );
+ } elseif ( !in_array( $transition,
$this->getAvailableTransitions() ) ) {
+ throw new InvalidTransitionException(
$this->getState(), $transition );
+ } else {
+ $destinationState =
$this->description->getDestinationState( $this->getState(), $transition );
+ }
+
+ try {
+ $this->exitState();
+
+ $this->enterState( $destinationState );
+ } catch ( TransitionNotPossibleException $ex ) {
+ $this->state = $this->previousState;
+ // TODO: reject transition, LOUDER
+
+ $this->nextSignal = null;
+ } catch ( Exception $ex ) {
+ // TODO: $this->handleException( $ex );
+ $this->nextSignal = null;
+
+ throw $ex;
+ }
+
+ $this->setBusy( false );
+ }
+
+ function getTitle() {
+ return $this->description->getTitle();
+ }
+
+ function getState() {
+ return $this->state;
+ }
+
+ protected function exitState() {
+ // TODO: begin transaction on context data
+ // TODO: system-global mutex in the db, guarantee atomicity
+ $this->setBusy( true );
+ $this->previousState = $this->state;
+ }
+
+ protected function enterState( $state ) {
+ $this->state = $state;
+
+ $actionList = $this->description->getActionsForState( $state );
+ foreach ( $actionList as $action => $params ) {
+ $this->handleAction( $action, $params );
+ }
+ }
+
+ protected function handleAction( $action, $params ) {
+ foreach ( $this->libraries as $library ) {
+ if ( in_array( $action, $library->getActions() ) ) {
+ $library->handleAction( $this, $action, $params
);
+ return;
+ }
+ }
+
+ throw new InvalidActionException( $action );
+ }
+
+ /**
+ * Retrieve a value from either the context, or from the
+ * description configuration.
+ */
+ function getValue( $key ) {
+ if ( $value = $this->context->getValue( $key ) ) {
+ return $value;
+ } else {
+ return $this->description->getConfigurationValue( $key
);
+ }
+ }
+
+ function setValue( $key, $value ) {
+ $this->context->setValue( $key, $value );
+ }
+
+ /**
+ * Returns all signals leading from the current node
+ */
+ protected function getAvailableTransitions() {
+ return $this->description->getTransitionsForState( $this->state
);
+ }
+
+ function isBusy() {
+ return $this->busy;
+ }
+
+ function isFinished() {
+ return ( count( $this->getAvailableTransitions() ) == 0 );
+ }
+
+ protected function setBusy( $busy ) {
+ if ( $busy ) {
+ if ( $this->isBusy() ) {
+ throw new BusyMachineException();
+ }
+ $this->busy = true;
+ } else {
+ // TODO: assert if double-unbusy
+ $this->busy = false;
+ }
+ }
+
+ function __sleep() {
+ $this->setBusy( true );
+ }
+
+ function __wakeup() {
+ $this->setBusy( false );
+ }
+}
diff --git a/includes/StateMachineDescription.php
b/includes/StateMachineDescription.php
new file mode 100644
index 0000000..d737b76
--- /dev/null
+++ b/includes/StateMachineDescription.php
@@ -0,0 +1,121 @@
+<?php namespace Workflow;
+
+use Symfony\Component\Yaml\Parser as YamlParser;
+
+/**
+ * Graph and configuration for a state machine
+ *
+ * The description contains all the information needed to instantiate a
+ * job.
+ */
+class StateMachineDescription {
+ // TODO: Entities as objects
+ protected $data;
+
+ static function loadFromFile( $path ) {
+ // TODO store hash?
+ return self::loadFromYaml( file_get_contents( $path ) );
+ }
+
+ // FIXME: untested
+ static function loadFromWiki( $title ) {
+ // TODO store revision ID
+ $page = new WikiPage( Title::newFromText( $title ) );
+ $data = $page->getContent();
+ return self::loadFromYaml( $data );
+ }
+
+ static function loadFromYaml( $str ) {
+ $parser = new YamlParser();
+ $data = $parser->parse( $str );
+
+ return new StateMachineDescription( $data );
+ }
+
+ static function validateDescriptionData( $data ) {
+ // TODO
+ }
+
+ protected function __construct( $data ) {
+ self::validateDescriptionData( $data );
+
+ // Apply defaults
+ $this->data = $data + array(
+ 'configuration' => array(),
+ 'exceptions' => array(),
+ 'libraries' => array(),
+ );
+
+ foreach ( $this->data['states'] as $state => &$info ) {
+ $info = (array)$info + array(
+ 'actions' => array(),
+ 'transitions' => array(),
+ );
+ }
+ }
+
+ function getInitialState() {
+ // ick. Relies on PHP's schizo internal representation of a map
+ reset( $this->data['states'] );
+ return key( $this->data['states'] );
+ }
+
+ function getTransitionsForState( $state ) {
+ $this->assertStateExists( $state );
+
+ return array_keys( $this->data['states'][$state]['transitions']
);
+ }
+
+ function getActionsForState( $state ) {
+ $this->assertStateExists( $state );
+
+ return $this->data['states'][$state]['actions'];
+ }
+
+ function getDestinationState( $state, $transition ) {
+ $this->assertStateExists( $state );
+ $this->assertTransitionExists( $state, $transition );
+
+ return
$this->data['states'][$state]['transitions'][$transition];
+ }
+
+ function getTitle() {
+ return $this->data['title'];
+ }
+
+ function getLibraries() {
+ return $this->data['libraries'];
+ }
+
+ function getExceptions() {
+ return array_keys( $this->data['exceptions'] );
+ }
+
+ function getExceptionDestination( $name ) {
+ return $this->data['exceptions'][$name];
+ }
+
+ function getConfigurationValue( $key ) {
+ $this->assertConfigurationExists( $key );
+
+ return $this->data['configuration'][$key];
+ }
+
+ protected function assertConfigurationExists( $key ) {
+ if ( !array_key_exists( $key, $this->data['configuration'] ) ) {
+ throw new InvalidVariableException( $key );
+ }
+ }
+
+ protected function assertStateExists( $state ) {
+ if ( !array_key_exists( $state, $this->data['states'] ) ) {
+ throw new InvalidStateException( $state );
+ }
+ }
+
+ protected function assertTransitionExists( $state, $transition ) {
+ if ( !array_key_exists( $transition,
$this->data['states'][$state]['transitions'] ) ) {
+ throw new InvalidTransitionException( $state,
$transition );
+ }
+ }
+}
diff --git a/includes/TransitionNotPossibleException.php
b/includes/TransitionNotPossibleException.php
new file mode 100644
index 0000000..9cc7da7
--- /dev/null
+++ b/includes/TransitionNotPossibleException.php
@@ -0,0 +1,11 @@
+<?php namespace Workflow;
+
+use \Exception;
+
+/**
+ * Special exception which can be safely thrown from an enterState action
+ *
+ * This exception will rollback the last transition.
+ */
+class TransitionNotPossibleException extends Exception {
+}
diff --git a/includes/library/BaseMachineLibrary.php
b/includes/library/BaseMachineLibrary.php
new file mode 100644
index 0000000..d9e52f2
--- /dev/null
+++ b/includes/library/BaseMachineLibrary.php
@@ -0,0 +1,28 @@
+<?php namespace Workflow\Library;
+
+use Workflow\InvalidActionException;
+use Workflow\IStateMachine;
+
+/**
+ * Base class with default dispatch
+ *
+ * Subclasses should define an $actions member (or override getActions), and
+ * a "handle_"- function for each action.
+ */
+class BaseMachineLibrary
+ implements IMachineLibrary
+{
+ protected $actions = array();
+
+ function handleAction( IStateMachine $machine, $action, $params ) {
+ if ( !in_array( $action, $this->getActions() ) ) {
+ throw new InvalidActionException( $action );
+ }
+ $func = "handle_{$action}";
+ $this->$func( $machine, (array)$params );
+ }
+
+ function getActions() {
+ return $this->actions;
+ }
+}
diff --git a/includes/library/IMachineLibrary.php
b/includes/library/IMachineLibrary.php
new file mode 100644
index 0000000..1352276
--- /dev/null
+++ b/includes/library/IMachineLibrary.php
@@ -0,0 +1,29 @@
+<?php namespace Workflow\Library;
+
+use Workflow\IStateMachine;
+
+/**
+ * Container for any PHP code that will be executed by a workflow.
+ *
+ * Multiple libraries can be mixed into a workflow.
+ *
+ * A library should not manage any data internally, but the machine context can
+ * be used for static storage.
+ */
+interface IMachineLibrary {
+ /**
+ * @return array list of actions provided by this library
+ */
+ function getActions();
+
+ /**
+ * Dispatch an action to this library
+ *
+ * @param IStateMachine $machine
+ * @param string $action name of action to be dispatched
+ * @param array $params Map or list of arguments. It is recommended to
always
+ * pass references to configuration and context variable names, rather
than raw
+ * string constant data.
+ */
+ function handleAction( IStateMachine $machine, $action, $params );
+}
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..1ee76ba
--- /dev/null
+++ b/includes/library/SelfStimulating.php
@@ -0,0 +1,24 @@
+<?php namespace Workflow\Library;
+
+/**
+ * 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
+ extends BaseMachineLibrary
+{
+ protected $actions = array(
+ 'signal',
+ );
+
+ /**
+ * @param array $params (
+ * string $signal - name of the signal to send
+ * );
+ */
+ function handle_signal( $machine, $params ) {
+ list( $signal ) = $params;
+
+ $machine->signal( $signal );
+ }
+}
diff --git a/includes/library/WikiPages.php b/includes/library/WikiPages.php
new file mode 100644
index 0000000..a1f7c5e
--- /dev/null
+++ b/includes/library/WikiPages.php
@@ -0,0 +1,40 @@
+<?php namespace Workflow\Library;
+
+/**
+ * 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..e2258a0
--- /dev/null
+++ b/tests/BaseWorkflowTestCase.php
@@ -0,0 +1,17 @@
+<?php namespace Workflow\Tests;
+
+use \MediaWikiTestCase;
+
+use Workflow\StateMachineDescription;
+use Workflow\StateMachine;
+
+class BaseWorkflowTestCase extends MediaWikiTestCase {
+ protected function setUp() {
+ parent::setUp();
+
+ $descriptionPath = __DIR__ . '/ennui_machine.yaml';
+
+ $this->description = StateMachineDescription::loadFromFile(
$descriptionPath );
+ $this->machine = new StateMachine( $this->description );
+ }
+}
diff --git a/tests/DoorLibrary.php b/tests/DoorLibrary.php
new file mode 100644
index 0000000..9a16396
--- /dev/null
+++ b/tests/DoorLibrary.php
@@ -0,0 +1,23 @@
+<?php namespace Workflow\Tests;
+
+use Workflow\IStateMachine;
+use Workflow\Library\BaseMachineLibrary;
+
+class DoorLibrary
+ extends BaseMachineLibrary
+{
+ const DOOR_VAR = 'door_locked';
+
+ protected $actions = array(
+ 'lock',
+ 'unlock',
+ );
+
+ function handle_lock( IStateMachine $machine ) {
+ $machine->setValue( self::DOOR_VAR, true );
+ }
+
+ function handle_unlock( IStateMachine $machine ) {
+ $machine->setValue( self::DOOR_VAR, false );
+ }
+}
diff --git a/tests/TestStateMachine.php b/tests/TestStateMachine.php
new file mode 100644
index 0000000..a4f61ba
--- /dev/null
+++ b/tests/TestStateMachine.php
@@ -0,0 +1,55 @@
+<?php namespace Workflow\Tests;
+
+/**
+ * @group Workflow
+ */
+class TestStateMachine extends BaseWorkflowTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->machine->begin();
+ }
+
+ function testInitialState() {
+ $this->assertEquals( 'inside', $this->machine->getState() );
+ }
+
+ function testTransition() {
+ $this->machine->signal( 'walk' );
+
+ $this->assertEquals( 'at_threshold', $this->machine->getState()
);
+ }
+
+ /**
+ * @expectedException Workflow\InvalidTransitionException
+ */
+ function testInvalidTransition() {
+ $this->machine->signal( 'bogon' );
+ }
+
+ function testExceptionSignal() {
+ $this->machine->signal( 'give_up' );
+
+ $this->assertEquals( 'trapped', $this->machine->getState() );
+ }
+
+ function testConfiguration() {
+ $this->assertSame( false, $this->machine->getValue(
'door_locked' ) );
+ }
+
+ function testAction() {
+ $this->machine->signal( 'walk' );
+ $this->machine->signal( 'use_key' );
+
+ $this->assertEquals( true, $this->machine->getValue(
'door_locked' ) );
+ }
+
+ function testCompletion() {
+ $this->machine->signal( 'walk' );
+ $this->machine->signal( 'open_door' );
+ $this->machine->signal( 'exit' );
+
+ $this->assertTrue( $this->machine->isFinished() );
+ }
+}
diff --git a/tests/TestStateMachineDescription.php
b/tests/TestStateMachineDescription.php
new file mode 100644
index 0000000..720edde
--- /dev/null
+++ b/tests/TestStateMachineDescription.php
@@ -0,0 +1,36 @@
+<?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() {
+ $this->assertEquals( array( 'walk', 'fidget' ),
$this->description->getTransitionsForState( 'inside' ) );
+ }
+
+ function testGetDestinationState() {
+ $this->assertEquals( 'at_threshold',
$this->description->getDestinationState( 'inside', 'walk' ) );
+ }
+
+ function testGetExceptions() {
+ $this->assertEquals( array( 'give_up' ),
$this->description->getExceptions() );
+ }
+
+ function testGetActionsForState() {
+ $this->assertEquals( array( 'lock' => null, 'signal' =>
'turned_key' ), $this->description->getActionsForState( 'key_in_cylinder' ) );
+ $this->assertSame( array(),
$this->description->getActionsForState( 'at_liberty' ) );
+ }
+
+ 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..88547f6
--- /dev/null
+++ b/tests/ennui_machine.yaml
@@ -0,0 +1,35 @@
+# 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: at_liberty
+ return: inside
+ use_key: key_in_cylinder
+ key_in_cylinder:
+ actions:
+ lock:
+ signal: turned_key
+ transitions:
+ turned_key: at_threshold
+ at_liberty:
+ transitions:
+ exit: gone
+ gone:
+ trapped:
+
+exceptions:
+ give_up: trapped
+
+configuration:
+ door_locked: false
--
To view, visit https://gerrit.wikimedia.org/r/107307
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I3f74158289f19efe15546bbacde79b40afed14c2
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Workflow
Gerrit-Branch: master
Gerrit-Owner: Adamw <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits