/** * @test */ public function shouldLoadTransitionsFromFile() { $machine = new StateMachine(new Context(new Identifier('xml-test', 'test-machine'))); $this->assertCount(0, $machine->getTransitions()); //this is a symbolic link to the asset/xml/example.xml file $loader = XML::createFromFile(__DIR__ . '/fixture-example.xml'); $count = $loader->load($machine); $this->assertCount(4, $machine->getTransitions(), 'there is a regex transition that adds 2 transitions (a-c and b-c)'); $this->assertEquals(4, $count); $this->assertEquals(0, MyStatic::$guard); $this->assertTrue($machine->ab()); $this->assertEquals(1, MyStatic::$guard, 'guard callable specified in xml should be called'); $this->assertTrue($machine->bdone()); $this->assertEquals(2, MyStatic::$entry, '2 entry state callables in config'); $this->assertEquals(1, MyStatic::$exit, '1 exit state callable in config'); }
/** * Checks a fully loaded statemachine for a valid configuration. * * This is useful in a debug situation so you can check for the validity of * rules, commands and callables in guard logic, transition logic and state entry/exit logic * before they hit a * * This does not instantiate any objects. It just checks if callables can be found * and if classes for rules and commands can be found * * @param StateMachine $machine * @return Exception[] an array of exceptions if anything is wrong with the configuration */ public static function checkConfiguration(StateMachine $machine) { //TODO: also check the rules and commands $exceptions = array(); $output = array(); //check state callables foreach ($machine->getStates() as $state) { $exceptions[] = self::getExceptionForCheckingCallable($state->getExitCallable(), State::CALLABLE_ENTRY, $state); $exceptions[] = self::getExceptionForCheckingCallable($state->getEntryCallable(), State::CALLABLE_ENTRY, $state); } //check transition callables foreach ($machine->getTransitions() as $transition) { $exceptions[] = self::getExceptionForCheckingCallable($transition->getGuardCallable(), Transition::CALLABLE_GUARD, $transition); $exceptions[] = self::getExceptionForCheckingCallable($transition->getTransitionCallable(), Transition::CALLABLE_TRANSITION, $transition); } //get the exceptions foreach ($exceptions as $e) { if (is_a($e, '\\Exception')) { $output[] = $e; } } return $output; }
/** * @test */ public function shouldLoadAndWriteViaDelegator() { $loader = XML::createFromFile(__DIR__ . '/../loader/fixture-example.xml'); $writer = new Memory(); $identifier = new Identifier('readerwriter-test', 'test-machine'); $delegator = new ReaderWriterDelegator($loader, $writer); $context = new Context($identifier, null, $delegator); $machine = new StateMachine($context); $this->assertCount(0, $machine->getTransitions()); $count = $delegator->load($machine); //add to the backend $this->assertTrue($context->add('a')); $this->assertCount(4, $machine->getTransitions(), 'there is a regex transition that adds 2 transitions (a-c and b-c)'); $this->assertEquals(4, $count); $this->assertTrue($machine->ab()); //get the data from the memory storage facility $data = $writer->getStorageFromRegistry($machine->getContext()->getIdentifier()); $this->assertEquals('b', $data->state); $this->assertEquals('b', $machine->getCurrentState()->getName()); $this->assertTrue($machine->bdone()); $data = $writer->getStorageFromRegistry($machine->getContext()->getIdentifier()); $this->assertEquals('done', $data->state); }
/** * @test */ public function shouldLoadTransitionsFromJSONString() { $machine = new StateMachine(new Context(new Identifier('json-test', 'json-machine'))); $this->assertCount(0, $machine->getTransitions()); $json = $this->getJSON(); $loader = new JSON($json); $this->assertEquals($this->getJSON(), $loader->getJSON()); $count = $loader->load($machine); $this->assertCount(2, $machine->getTransitions()); $this->assertEquals(2, $count); $tbd = $machine->getTransition('b_to_done'); $b = $tbd->getStateFrom(); $d = $tbd->getStateTo(); $tab = $machine->getTransition('a_to_b'); $a = $tab->getStateFrom(); $this->assertEquals($b, $tab->getStateTo()); $this->assertSame($b, $tab->getStateTo()); $this->assertTrue($a->isInitial()); $this->assertTrue($b->isNormal()); $this->assertTrue($d->isFinal()); }
/** * @test */ public function shouldAddRegexesLoaderOnlyWhenStatesAreSet() { $context = new Context(new Identifier(Identifier::NULL_ENTITY_ID, Identifier::NULL_STATEMACHINE)); $machine = new StateMachine($context); $transitions = array(); $s1 = new State("1"); $s2 = new State("2"); $s3 = new State("3"); //many to many $transitions[] = new Transition(new State('regex:/.*/'), new State('regex:/.*/')); $loader = new LoaderArray($transitions); $this->assertEquals(count($transitions), $loader->count()); $this->assertEquals(1, $loader->count()); $this->assertEquals(0, count($machine->getStates())); $this->assertEquals(0, count($machine->getTransitions())); $count = $loader->load($machine); $this->assertEquals(0, $count, 'nothing because there are no known states'); $this->assertTrue($machine->addState($s1)); $this->assertTrue($machine->addState($s2)); $this->assertTrue($machine->addState($s3)); $this->assertEquals(3, count($machine->getStates())); $this->assertFalse($machine->addState($s1)); $this->assertFalse($machine->addState($s2)); $this->assertFalse($machine->addState($s3)); $this->assertEquals(3, count($machine->getStates())); $count = $loader->load($machine); $this->assertEquals(6, count($machine->getTransitions())); $this->assertEquals(6, $count, 'regexes have matched all states and created a mesh'); $count = $loader->load($machine); $this->assertEquals(0, $count, 'transitions are not added since they have already been added'); $this->assertEquals(3, count($machine->getStates())); $this->assertEquals(6, count($machine->getTransitions())); }
/** * creates plantuml state output for a statemachine * * @param string $machine * @return string plant uml code, this can be used to render an image * @link http://www.plantuml.com/plantuml/ * @link http://plantuml.sourceforge.net/state.html * @throws Exception */ public function createStateDiagram(StateMachine $machine) { $transitions = $machine->getTransitions(); // all states are aliased so the plantuml parser can handle the names $aliases = array(); $end_states = array(); $EOL = "\\n\\" . PHP_EOL; /* for multiline stuff in plantuml */ $NEWLINE = PHP_EOL; // start with declaration $uml = "@startuml" . PHP_EOL; // skins for colors etc. $uml .= $this->getPlantUmlSkins() . PHP_EOL; // the order in which transitions are executed $order = array(); // create the diagram by drawing all transitions foreach ($transitions as $t) { // get states and state aliases (plantuml cannot work with certain // characters, so therefore we create an alias for the state name) $from = $t->getStateFrom(); $from_alias = $this->plantUmlStateAlias($from->getName()); $to = $t->getStateTo(); $to_alias = $this->plantUmlStateAlias($to->getName()); // get some names to display $command = $t->getCommandName(); $rule = self::escape($t->getRuleName()); $name_transition = $t->getName(); $description = $t->getDescription() ? "description: '" . $t->getDescription() . "'" . $EOL : ''; $event = $t->getEvent() ? "event: '" . $t->getEvent() . "'" . $EOL : ''; $f_description = $from->getDescription(); $t_description = $to->getDescription(); $f_exit = $from->getExitCommandName(); $f_entry = $from->getEntryCommandName(); $t_exit = $to->getExitCommandName(); $t_entry = $to->getEntryCommandName(); // only write aliases if not done before if (!isset($aliases[$from_alias])) { $uml .= 'state "' . $from . '" as ' . $from_alias . PHP_EOL; $uml .= "{$from_alias}: description: '" . $f_description . "'" . PHP_EOL; $uml .= "{$from_alias}: entry / '" . $f_entry . "'" . PHP_EOL; $uml .= "{$from_alias}: exit / '" . $f_exit . "'" . PHP_EOL; $aliases[$from_alias] = $from_alias; } // store order in which transitions will be handled if (!isset($order[$from_alias])) { $order[$from_alias] = 1; } else { $order[$from_alias] = $order[$from_alias] + 1; } // get 'to' alias if (!isset($aliases[$to_alias])) { $uml .= 'state "' . $to . '" as ' . $to_alias . PHP_EOL; $aliases[$to_alias] = $to_alias; $uml .= "{$to_alias}: description: '" . $t_description . "'" . PHP_EOL; $uml .= "{$to_alias}: entry / '" . $t_entry . "'" . PHP_EOL; $uml .= "{$to_alias}: exit / '" . $t_exit . "'" . PHP_EOL; } // write transition information $uml .= $from_alias . ' --> ' . $to_alias; $uml .= " : <b><size:10>{$name_transition}</size></b>" . $EOL; $uml .= $event; $uml .= "transition order from '{$from}': <b>" . $order[$from_alias] . "</b>" . $EOL; $uml .= "rule/guard: '{$rule}'" . $EOL; $uml .= "command/action: '{$command}'" . $EOL; $uml .= $description; $uml .= PHP_EOL; // store possible end states aliases if ($t->getStateFrom()->isFinal()) { $end_states[$from_alias] = $from_alias; } if ($t->getStateTo()->isFinal()) { $end_states[$to_alias] = $to_alias; } } // only one begin state $initial = $machine->getInitialState(); $initial = $initial->getName(); $initial_alias = $this->plantUmlStateAlias($initial); if (!isset($aliases[$initial_alias])) { $uml .= 'state "' . $initial . '" as ' . $initial_alias . PHP_EOL; } $uml .= "[*] --> {$initial_alias}" . PHP_EOL; // note for initial alias with explanation $uml .= "note right of {$initial_alias} {$NEWLINE}"; $uml .= "state diagram for machine '" . $machine->getMachine() . "'{$NEWLINE}"; $uml .= "created by izzum plantuml generator {$NEWLINE}"; $uml .= "@link http://plantuml.sourceforge.net/state.html\"" . $NEWLINE; $uml .= "end note" . $NEWLINE; // add end states to diagram foreach ($end_states as $end) { $uml .= "{$end} --> [*]" . PHP_EOL; } // close plantuml $uml .= "@enduml" . PHP_EOL; return $uml; }