diff --git a/src/Model/Bug/Step.php b/src/Model/Bug/Step.php index ea95112f..4322cb29 100644 --- a/src/Model/Bug/Step.php +++ b/src/Model/Bug/Step.php @@ -42,15 +42,9 @@ public function __clone() public function getUniqueNodeId(): string { - $places = $this->places; - ksort($places); - $colorValues = $this->color->getValues(); - ksort($colorValues); - - return md5(serialize([ - 'places' => $places, - 'color' => $colorValues, - ])); + ksort($this->places); + + return md5(serialize($this->places)); } public function setColor(ColorInterface $color): void diff --git a/src/Reducer/DispatcherTemplate.php b/src/Reducer/DispatcherTemplate.php index 7c5c7459..1356077b 100644 --- a/src/Reducer/DispatcherTemplate.php +++ b/src/Reducer/DispatcherTemplate.php @@ -8,8 +8,6 @@ abstract class DispatcherTemplate implements DispatcherInterface { - protected const MIN_PAIR_LENGTH = 2; // 3 steps - protected MessageBusInterface $messageBus; public function __construct(MessageBusInterface $messageBus) diff --git a/src/Reducer/Random/RandomDispatcher.php b/src/Reducer/Random/RandomDispatcher.php index 995bb262..7be3af30 100644 --- a/src/Reducer/Random/RandomDispatcher.php +++ b/src/Reducer/Random/RandomDispatcher.php @@ -14,7 +14,7 @@ protected function getPairs(array $steps): array while (count($pairs) < $maxPairs) { $pair = array_rand(range(0, $length - 1), 2); - if ($pair[1] - $pair[0] >= static::MIN_PAIR_LENGTH && !in_array($pair, $pairs)) { + if (!in_array($pair, $pairs)) { $pairs[] = $pair; } } diff --git a/src/Reducer/Split/SplitDispatcher.php b/src/Reducer/Split/SplitDispatcher.php index ebfa9e11..8457f413 100644 --- a/src/Reducer/Split/SplitDispatcher.php +++ b/src/Reducer/Split/SplitDispatcher.php @@ -17,9 +17,7 @@ protected function getPairs(array $steps): array $range[] = $length - 1; } for ($i = 0; $i < count($range) - 1; ++$i) { - if ($range[$i + 1] - $range[$i] >= static::MIN_PAIR_LENGTH) { - $pairs[] = [$range[$i], $range[$i + 1]]; - } + $pairs[] = [$range[$i], $range[$i + 1]]; } return $pairs; diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index a8a279cf..bafa7aed 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -45,8 +45,6 @@ use Tienvx\Bundle\MbtBundle\Repository\BugRepositoryInterface; use Tienvx\Bundle\MbtBundle\Repository\TaskRepository; use Tienvx\Bundle\MbtBundle\Repository\TaskRepositoryInterface; -use Tienvx\Bundle\MbtBundle\Service\AStar\PetrinetDomainLogic; -use Tienvx\Bundle\MbtBundle\Service\AStar\PetrinetDomainLogicInterface; use Tienvx\Bundle\MbtBundle\Service\Bug\BugHelper; use Tienvx\Bundle\MbtBundle\Service\Bug\BugHelperInterface; use Tienvx\Bundle\MbtBundle\Service\Bug\BugNotifierInterface; @@ -247,16 +245,10 @@ ->set(ShortestPathStepsBuilder::class) ->args([ service(PetrinetHelperInterface::class), - service(PetrinetDomainLogicInterface::class), - ]) - ->alias(StepsBuilderInterface::class, ShortestPathStepsBuilder::class) - - ->set(PetrinetDomainLogic::class) - ->args([ service(GuardedTransitionServiceInterface::class), service(MarkingHelperInterface::class), ]) - ->alias(PetrinetDomainLogicInterface::class, PetrinetDomainLogic::class) + ->alias(StepsBuilderInterface::class, ShortestPathStepsBuilder::class) ->set(StepRunner::class) ->args([ diff --git a/src/Service/AStar/PetrinetDomainLogicInterface.php b/src/Service/AStar/PetrinetDomainLogicInterface.php deleted file mode 100644 index 1df6aecc..00000000 --- a/src/Service/AStar/PetrinetDomainLogicInterface.php +++ /dev/null @@ -1,11 +0,0 @@ -transitionService = $transitionService; $this->markingHelper = $markingHelper; - } - - public function setPetrinet(?PetrinetInterface $petrinet): void - { $this->petrinet = $petrinet; } @@ -46,8 +44,8 @@ public function calculateEstimatedCost(mixed $fromNode, mixed $toNode): float|in $tokensDiff += abs($toNode->getPlaces()[$place] - $fromNode->getPlaces()[$place]); } } - // Estimate it will took N transitions to move N tokens if color is the same, twice if color is not the same. - return $tokensDiff * (($fromNode->getColor()->getValues() != $toNode->getColor()->getValues()) + 1); + // Estimate it will took N transitions to move N tokens. + return $tokensDiff; } public function calculateRealCost(mixed $node, mixed $adjacent): float|int @@ -62,10 +60,6 @@ public function getAdjacentNodes(mixed $node): iterable throw new RuntimeException('The provided node is invalid'); } - if (!$this->petrinet instanceof PetrinetInterface) { - throw new RuntimeException('Petrinet is required'); - } - $adjacents = []; $marking = $this->markingHelper->getMarking($this->petrinet, $node->getPlaces(), $node->getColor()); foreach ($this->transitionService->getEnabledTransitions($this->petrinet, $marking) as $transition) { diff --git a/src/Service/Step/Builder/ShortestPathStepsBuilder.php b/src/Service/Step/Builder/ShortestPathStepsBuilder.php index 738a602c..c12bcade 100644 --- a/src/Service/Step/Builder/ShortestPathStepsBuilder.php +++ b/src/Service/Step/Builder/ShortestPathStepsBuilder.php @@ -4,24 +4,31 @@ use Generator; use JMGQ\AStar\AStar; +use RuntimeException; +use SingleColorPetrinet\Model\PetrinetInterface; +use SingleColorPetrinet\Service\GuardedTransitionServiceInterface; use Tienvx\Bundle\MbtBundle\Exception\ExceptionInterface; use Tienvx\Bundle\MbtBundle\Exception\OutOfRangeException; +use Tienvx\Bundle\MbtBundle\Model\Bug\Step; use Tienvx\Bundle\MbtBundle\Model\Bug\StepInterface; use Tienvx\Bundle\MbtBundle\Model\BugInterface; -use Tienvx\Bundle\MbtBundle\Service\AStar\PetrinetDomainLogicInterface; +use Tienvx\Bundle\MbtBundle\Service\Petrinet\MarkingHelperInterface; use Tienvx\Bundle\MbtBundle\Service\Petrinet\PetrinetHelperInterface; class ShortestPathStepsBuilder implements StepsBuilderInterface { protected PetrinetHelperInterface $petrinetHelper; - protected PetrinetDomainLogicInterface $petrinetDomainLogic; + protected GuardedTransitionServiceInterface $transitionService; + protected MarkingHelperInterface $markingHelper; public function __construct( PetrinetHelperInterface $petrinetHelper, - PetrinetDomainLogicInterface $petrinetDomainLogic + GuardedTransitionServiceInterface $transitionService, + MarkingHelperInterface $markingHelper ) { $this->petrinetHelper = $petrinetHelper; - $this->petrinetDomainLogic = $petrinetDomainLogic; + $this->transitionService = $transitionService; + $this->markingHelper = $markingHelper; } /** @@ -30,23 +37,46 @@ public function __construct( public function create(BugInterface $bug, int $from, int $to): Generator { yield from array_slice($bug->getSteps(), 0, $from); - yield from $this->getSteps($bug, $from, $to); - yield from array_slice($bug->getSteps(), $to + 1); + $petrinet = $this->petrinetHelper->build($bug->getTask()->getModelRevision()); + $shortestSteps = $this->getShortestSteps($bug->getSteps(), $from, $to, $petrinet); + $lastStep = end($shortestSteps); + reset($shortestSteps); + yield from $shortestSteps; + yield from $this->getRemainingSteps(array_slice($bug->getSteps(), $to + 1), $lastStep, $petrinet); } - protected function getSteps(BugInterface $bug, int $from, int $to): iterable + protected function getShortestSteps(array $steps, int $from, int $to, PetrinetInterface $petrinet): iterable { - $fromStep = $bug->getSteps()[$from] ?? null; - $toStep = $bug->getSteps()[$to] ?? null; + $fromStep = $steps[$from] ?? null; + $toStep = $steps[$to] ?? null; if (!$fromStep instanceof StepInterface || !$toStep instanceof StepInterface) { - throw new OutOfRangeException('Can not create new steps using invalid range'); + throw new OutOfRangeException('Can not create shortest steps between invalid range'); } - $this->petrinetDomainLogic->setPetrinet($this->petrinetHelper->build($bug->getTask()->getModelRevision())); - - yield from (new AStar($this->petrinetDomainLogic))->run($fromStep, $toStep); + return (new AStar(new PetrinetDomainLogic($this->transitionService, $this->markingHelper, $petrinet)))->run( + $fromStep, + $toStep + ); + } - $this->petrinetDomainLogic->setPetrinet(null); + protected function getRemainingSteps(array $steps, StepInterface $lastStep, PetrinetInterface $petrinet): iterable + { + $marking = $this->markingHelper->getMarking($petrinet, $lastStep->getPlaces(), $lastStep->getColor()); + foreach ($steps as $step) { + if (!$step instanceof StepInterface) { + throw new OutOfRangeException('Remaining steps contains invalid step'); + } + $transition = $petrinet->getTransitionById($step->getTransition()); + if (!$this->transitionService->isEnabled($transition, $marking)) { + throw new RuntimeException('Can not connect remaining steps'); + } + $this->transitionService->fire($transition, $marking); + yield new Step( + $this->markingHelper->getPlaces($marking), + $marking->getColor(), + $step->getTransition() + ); + } } } diff --git a/tests/Model/Bug/StepTest.php b/tests/Model/Bug/StepTest.php index d9fb84c8..0e0b7a70 100644 --- a/tests/Model/Bug/StepTest.php +++ b/tests/Model/Bug/StepTest.php @@ -55,24 +55,19 @@ public function testClone(): void /** * @dataProvider nodeIdProvider */ - public function testGetUniqueNodeId(?array $places, ?ColorInterface $color, string $id): void + public function testGetUniqueNodeId(?array $places, string $id): void { if ($places) { $this->step->setPlaces($places); } - if ($color) { - $this->step->setColor($color); - } $this->assertSame($id, $this->step->getUniqueNodeId()); } public function nodeIdProvider(): array { return [ - [null, null, 'f179bfa0d0b5b6751e353f049461eda8'], - [null, new Color(['key1' => 'value1']), 'e13d72c92c38781375d3a400df07d43a'], - [[0 => 2, 1 => 1], null, 'e1b90c9311d5bd1d7fc90fd43d9bd49f'], - [[0 => 1, 1 => 1], new Color(['key2' => 'value2']), '61a579e02eb3ae787ef03ad40feb9a7d'], + [null, '89fefb193877ee62e29d1da5975dcc47'], + [[0 => 2, 1 => 1], '02878487ecf2302bf7ba2cc919514889'], ]; } diff --git a/tests/Reducer/DispatcherTestCase.php b/tests/Reducer/DispatcherTestCase.php index 075db5c5..9629eefe 100644 --- a/tests/Reducer/DispatcherTestCase.php +++ b/tests/Reducer/DispatcherTestCase.php @@ -56,7 +56,6 @@ protected function assertMessage(int $length): Callback } return $message->getBugId() === $this->bug->getId() && - $message->getFrom() + 2 <= $message->getTo() && $length === $message->getLength(); }); } diff --git a/tests/Reducer/Split/SplitDispatcherTest.php b/tests/Reducer/Split/SplitDispatcherTest.php index 54ff22b9..2bb00db8 100644 --- a/tests/Reducer/Split/SplitDispatcherTest.php +++ b/tests/Reducer/Split/SplitDispatcherTest.php @@ -37,6 +37,7 @@ public function stepsProvider(): array [6, [ [0, 2], [2, 4], + [4, 5], ]], [7, [ [0, 3], @@ -45,6 +46,7 @@ public function stepsProvider(): array [8, [ [0, 3], [3, 6], + [6, 7], ]], [9, [ [0, 3], @@ -60,6 +62,7 @@ public function stepsProvider(): array [0, 3], [3, 6], [6, 9], + [9, 10], ]], [12, [ [0, 3], diff --git a/tests/Service/AStar/PetrinetDomainLogicTest.php b/tests/Service/Step/Builder/PetrinetDomainLogicTest.php similarity index 83% rename from tests/Service/AStar/PetrinetDomainLogicTest.php rename to tests/Service/Step/Builder/PetrinetDomainLogicTest.php index 509d1a28..08593ab4 100644 --- a/tests/Service/AStar/PetrinetDomainLogicTest.php +++ b/tests/Service/Step/Builder/PetrinetDomainLogicTest.php @@ -1,6 +1,6 @@ transitionService = $this->createMock(GuardedTransitionServiceInterface::class); $this->markingHelper = $this->createMock(MarkingHelperInterface::class); - $this->petrinetDomainLogic = new PetrinetDomainLogic($this->transitionService, $this->markingHelper); $this->petrinet = $this->createMock(PetrinetInterface::class); + $this->petrinetDomainLogic = new PetrinetDomainLogic( + $this->transitionService, + $this->markingHelper, + $this->petrinet + ); $this->transitions = [ $transition1 = new GuardedTransition(), $transition2 = new GuardedTransition(), @@ -98,19 +101,11 @@ public function testCalculateEstimatedCost(Step $fromNode, Step $toNode, int $co public function estimatedCostProvider(): array { $color = new Color(['key' => 'value']); - $differentColor = new Color(['different key' => 'different value']); return [ [$this->getStep([0 => 1, 1 => 5, 2 => 3], $color), $this->getStep([], $color), 9], [$this->getStep([], $color), $this->getStep([0 => 3, 1 => 2, 2 => 2, 3 => 1], $color), 8], [$this->getStep([0 => 2, 1 => 4], $color), $this->getStep([1 => 5, 2 => 3, 3 => 1], $color), 7], - [$this->getStep([0 => 4, 1 => 1, 2 => 2], $color), $this->getStep([2 => 5], $differentColor), 16], - [$this->getStep([], $color), $this->getStep([0 => 4, 1 => 1], $differentColor), 10], - [ - $this->getStep([0 => 3, 1 => 2, 2 => 2], $color), - $this->getStep([1 => 7, 2 => 2, 3 => 3], $differentColor), - 22, - ], ]; } @@ -125,16 +120,9 @@ public function testGetAdjacentNodesOfInvalidNode(): void $this->petrinetDomainLogic->getAdjacentNodes('invalid'); } - public function testGetAdjacentNodesWithoutPetrinet(): void - { - $this->expectExceptionObject(new RuntimeException('Petrinet is required')); - $this->petrinetDomainLogic->getAdjacentNodes($this->getStep([], new Color())); - } - public function testGetAdjacentNodes(): void { $node = $this->getStep([12 => 34], new Color(['key' => 'value'])); - $this->petrinetDomainLogic->setPetrinet($this->petrinet); $this->markingHelper ->expects($this->exactly(count($this->transitions) + 1)) ->method('getMarking') diff --git a/tests/Service/Step/Builder/ShortestPathStepsBuilderTest.php b/tests/Service/Step/Builder/ShortestPathStepsBuilderTest.php index 7ffbf068..e407be5f 100644 --- a/tests/Service/Step/Builder/ShortestPathStepsBuilderTest.php +++ b/tests/Service/Step/Builder/ShortestPathStepsBuilderTest.php @@ -7,6 +7,7 @@ use Petrinet\Model\TransitionInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use RuntimeException; use SingleColorPetrinet\Builder\SingleColorPetrinetBuilder; use SingleColorPetrinet\Model\Color; use SingleColorPetrinet\Model\ColorfulFactory; @@ -20,10 +21,10 @@ use Tienvx\Bundle\MbtBundle\Entity\Task; use Tienvx\Bundle\MbtBundle\Exception\OutOfRangeException; use Tienvx\Bundle\MbtBundle\Model\Bug\Step; -use Tienvx\Bundle\MbtBundle\Service\AStar\PetrinetDomainLogic; use Tienvx\Bundle\MbtBundle\Service\Petrinet\MarkingHelper; use Tienvx\Bundle\MbtBundle\Service\Petrinet\MarkingHelperInterface; use Tienvx\Bundle\MbtBundle\Service\Petrinet\PetrinetHelperInterface; +use Tienvx\Bundle\MbtBundle\Service\Step\Builder\PetrinetDomainLogic; use Tienvx\Bundle\MbtBundle\Service\Step\Builder\ShortestPathStepsBuilder; /** @@ -34,7 +35,7 @@ * @uses \Tienvx\Bundle\MbtBundle\Model\Bug * @uses \Tienvx\Bundle\MbtBundle\Model\Bug\Step * @uses \Tienvx\Bundle\MbtBundle\Model\Task - * @uses \Tienvx\Bundle\MbtBundle\Service\AStar\PetrinetDomainLogic + * @uses \Tienvx\Bundle\MbtBundle\Service\Step\Builder\PetrinetDomainLogic * @uses \Tienvx\Bundle\MbtBundle\Service\ExpressionLanguage * @uses \Tienvx\Bundle\MbtBundle\Service\Petrinet\MarkingHelper */ @@ -67,6 +68,7 @@ class ShortestPathStepsBuilderTest extends TestCase protected TransitionInterface $chooseShipping; protected TransitionInterface $choosePayment; protected TransitionInterface $confirmOrder; + protected array $fromEmptyCartToConfirmOrderNodes; protected function setUp(): void { @@ -77,9 +79,9 @@ protected function setUp(): void $this->initPlaces($builder); $this->initTransitions($builder); $this->initPetrinet($builder); - $this->initPetrinetDomainLogic(); $this->initBug(); $this->initStepsBuilder(); + $this->initResults(); } protected function initPlaces(SingleColorPetrinetBuilder $builder): void @@ -161,12 +163,6 @@ protected function initPetrinet(SingleColorPetrinetBuilder $builder): void ->getPetrinet(); } - protected function initPetrinetDomainLogic(): void - { - $this->petrinetDomainLogic = new PetrinetDomainLogic($this->transitionService, $this->markingHelper); - $this->petrinetDomainLogic->setPetrinet($this->petrinet); - } - protected function initBug(): void { $this->bug = new Bug(); @@ -203,44 +199,16 @@ protected function geColor(int $products): Color protected function initStepsBuilder(): void { $this->petrinetHelper = $this->createMock(PetrinetHelperInterface::class); - $this->stepsBuilder = new ShortestPathStepsBuilder($this->petrinetHelper, $this->petrinetDomainLogic); - } - - protected function expectsPetrinetHelper(): void - { - $this->petrinetHelper - ->expects($this->once()) - ->method('build') - ->with($this->revision) - ->willReturn($this->petrinet); - } - - /** - * @dataProvider invalidRangeProvider - */ - public function testGetInvalidRange(int $from, int $to): void - { - $this->expectExceptionObject(new OutOfRangeException('Can not create new steps using invalid range')); - iterator_to_array($this->stepsBuilder->create($this->bug, $from, $to)); + $this->stepsBuilder = new ShortestPathStepsBuilder( + $this->petrinetHelper, + $this->transitionService, + $this->markingHelper + ); } - public function invalidRangeProvider(): array + protected function initResults(): void { - $validMinFrom = 0; - $validMaxTo = 16; - - return [ - [-1, $validMaxTo], - [$validMinFrom, 17], - [-1, 17], - ]; - } - - public function testGetShortestPathFromCartEmptyToCheckout(): void - { - $this->expectsPetrinetHelper(); - $nodes = $this->stepsBuilder->create($this->bug, 0, 12); - $this->assertNodes([ + $this->fromEmptyCartToConfirmOrderNodes = [ [ 'transition' => $this->clearCart->getId(), 'places' => [$this->cartEmpty->getId() => 1], @@ -276,7 +244,58 @@ public function testGetShortestPathFromCartEmptyToCheckout(): void 'places' => [$this->order->getId() => 1], 'color' => ['products' => 1], ], - ], $nodes); + ]; + } + + protected function expectsPetrinetHelper(): void + { + $this->petrinetHelper + ->expects($this->once()) + ->method('build') + ->with($this->revision) + ->willReturn($this->petrinet); + } + + /** + * @dataProvider invalidRangeProvider + */ + public function testGetInvalidRange(int $from, int $to): void + { + $this->expectExceptionObject(new OutOfRangeException('Can not create shortest steps between invalid range')); + iterator_to_array($this->stepsBuilder->create($this->bug, $from, $to)); + } + + public function invalidRangeProvider(): array + { + $validMinFrom = 0; + $validMaxTo = 16; + + return [ + [-1, $validMaxTo], + [$validMinFrom, 17], + [-1, 17], + ]; + } + + public function testGetShortestPathFromCartEmptyToCheckout(): void + { + $this->expectsPetrinetHelper(); + $nodes = $this->stepsBuilder->create($this->bug, 0, 12); + $this->assertNodes($this->fromEmptyCartToConfirmOrderNodes, $nodes); + } + + public function testGetShortestPathBetweenSameSteps(): void + { + $this->expectsPetrinetHelper(); + $nodes = $this->stepsBuilder->create($this->bug, 0, 10); + $this->assertNodes($this->fromEmptyCartToConfirmOrderNodes, $nodes); + } + + public function testGetShortestPathFromCartEmptyToFourProductsInCart(): void + { + $this->expectsPetrinetHelper(); + $this->expectExceptionObject(new RuntimeException('Can not connect remaining steps')); + iterator_to_array($this->stepsBuilder->create($this->bug, 0, 6)); } public function testGetShortestPathFromCartHasProductsToShipping(): void @@ -309,40 +328,30 @@ public function testGetShortestPathFromCartHasProductsToShipping(): void 'places' => [$this->cartHasProducts->getId() => 1], 'color' => ['products' => 4], ], - [ - 'transition' => $this->clearCart->getId(), - 'places' => [$this->cartEmpty->getId() => 1], - 'color' => ['products' => 0], - ], - [ - 'transition' => $this->addFirstProduct->getId(), - 'places' => [$this->cartHasProducts->getId() => 1], - 'color' => ['products' => 1], - ], [ 'transition' => $this->goToCheckout->getId(), 'places' => [$this->checkout->getId() => 1], - 'color' => ['products' => 1], + 'color' => ['products' => 4], ], [ 'transition' => $this->fillAddress->getId(), 'places' => [$this->address->getId() => 1], - 'color' => ['products' => 1], + 'color' => ['products' => 4], ], [ 'transition' => $this->chooseShipping->getId(), 'places' => [$this->shipping->getId() => 1], - 'color' => ['products' => 1], + 'color' => ['products' => 4], ], [ 'transition' => $this->choosePayment->getId(), 'places' => [$this->payment->getId() => 1], - 'color' => ['products' => 1], + 'color' => ['products' => 4], ], [ 'transition' => $this->confirmOrder->getId(), 'places' => [$this->order->getId() => 1], - 'color' => ['products' => 1], + 'color' => ['products' => 4], ], ], $nodes); }