Skip to content

Commit

Permalink
Merge pull request #196 from ProcessMaker/feature/empty_input_collection
Browse files Browse the repository at this point in the history
Skip MultiInstance Activity if Input is empty
  • Loading branch information
boliviacoca authored Apr 27, 2021
2 parents b6f9855 + bab58fe commit 2edd797
Show file tree
Hide file tree
Showing 11 changed files with 431 additions and 4 deletions.
25 changes: 25 additions & 0 deletions src/ProcessMaker/Nayra/Bpmn/ActivityTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ trait ActivityTrait
*/
private $closedState;

/**
*
* @var \ProcessMaker\Nayra\Contracts\Bpmn\StateInterface
*/
private $skippedState;

/**
*
* @var \ProcessMaker\Nayra\Contracts\Bpmn\TransitionInterface
*/
private $skippedTransition;

/**
* Build the transitions that define the element.
*
Expand All @@ -82,6 +94,8 @@ public function buildTransitions(RepositoryInterface $factory)
$this->loopTransition = new LoopCharacteristicsTransition($this, false);
$this->closedState->connectTo($this->loopTransition);
$this->loopTransition->connectTo($this->activeState);
$this->skippedState = new State($this, ActivityInterface::TOKEN_STATE_SKIPPED);
$this->skippedTransition = new Transition($this, false);

$this->activeState->connectTo($this->exceptionTransition);
$this->activeState->connectTo($this->activityTransition);
Expand All @@ -92,6 +106,7 @@ public function buildTransitions(RepositoryInterface $factory)
$this->activityTransition->connectTo($this->closedState);
$this->closedState->connectTo($this->transition);
$this->completeExceptionTransition->connectTo($this->closedState);
$this->skippedState->connectTo($this->skippedTransition);

$this->activeState->attachEvent(
StateInterface::EVENT_TOKEN_ARRIVED,
Expand Down Expand Up @@ -130,6 +145,12 @@ function (TokenInterface $token) {
$this->notifyEvent(ActivityInterface::EVENT_ACTIVITY_COMPLETED, $this, $token);
}
);
$this->skippedState->attachEvent(
StateInterface::EVENT_TOKEN_ARRIVED,
function (TokenInterface $token) {
$this->notifyEvent(ActivityInterface::EVENT_ACTIVITY_SKIPPED, $this, $token);
}
);
$this->closeExceptionTransition->attachEvent(
TransitionInterface::EVENT_AFTER_CONSUME,
function ($transition, $tokens) {
Expand Down Expand Up @@ -173,8 +194,11 @@ public function getInputPlace(FlowInterface $targetFlow = null)
{
$ready = new State($this, 'INCOMING');
$transition = new DataInputTransition($this, false);
$emptyDataInput = new EmptyDataInputTransition($this, false);
$ready->connectTo($transition);
$ready->connectTo($emptyDataInput);
$transition->connectTo($this->activeState);
$emptyDataInput->connectTo($this->skippedState);
$this->addInput($ready);
$transition->setProperty('sequenceFlow', $targetFlow);
return $ready;
Expand Down Expand Up @@ -204,6 +228,7 @@ function (TransitionInterface $transition, Collection $consumedTokens) {
}
}
);
$this->skippedTransition->connectTo($place);
return $this;
}

Expand Down
7 changes: 5 additions & 2 deletions src/ProcessMaker/Nayra/Bpmn/DataInputTransition.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@
use ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\CollectionInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\ConnectionInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\StateInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\TokenInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\TransitionInterface;
use ProcessMaker\Nayra\Contracts\Engine\ExecutionInstanceInterface;

/**
* Transition rule that always pass the token.
* Transition to check if the activity is a loop not yet completed or a single instance
*
* @package ProcessMaker\Nayra\Bpmn
*/
Expand All @@ -29,6 +28,10 @@ class DataInputTransition implements TransitionInterface
*/
public function assertCondition(TokenInterface $token = null, ExecutionInstanceInterface $executionInstance = null)
{
$loop = $this->getOwner()->getLoopCharacteristics();
if ($loop && $loop->isExecutable()) {
return !$loop->isLoopCompleted($executionInstance, $token);
}
return true;
}

Expand Down
67 changes: 67 additions & 0 deletions src/ProcessMaker/Nayra/Bpmn/EmptyDataInputTransition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace ProcessMaker\Nayra\Bpmn;

use ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\CollectionInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\ConnectionInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\TokenInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\TransitionInterface;
use ProcessMaker\Nayra\Contracts\Engine\ExecutionInstanceInterface;

/**
* Transition to check if the activity is a loop and the loop is completed
*
* @package ProcessMaker\Nayra\Bpmn
*/
class EmptyDataInputTransition implements TransitionInterface
{
use TransitionTrait;

/**
* Condition required to transit the element.
*
* @param \ProcessMaker\Nayra\Contracts\Bpmn\TokenInterface|null $token
* @param \ProcessMaker\Nayra\Contracts\Engine\ExecutionInstanceInterface|null $executionInstance
*
* @return bool
*/
public function assertCondition(TokenInterface $token = null, ExecutionInstanceInterface $executionInstance = null)
{
$loop = $this->getOwner()->getLoopCharacteristics();
$isLoopCompleted = $loop && $loop->isExecutable() && $loop->isLoopCompleted($executionInstance, $token);
return $isLoopCompleted;
}

/**
* Get transition owner element
*
* @return ActivityInterface
*/
public function getOwner()
{
return $this->owner;
}

// /**
// * Activate the next state.
// *
// * @param \ProcessMaker\Nayra\Contracts\Bpmn\ConnectionInterface $flow
// * @param \ProcessMaker\Nayra\Contracts\Engine\ExecutionInstanceInterface $instance
// * @param \ProcessMaker\Nayra\Contracts\Bpmn\CollectionInterface $consumeTokens
// * @param array $properties
// * @param \ProcessMaker\Nayra\Contracts\Bpmn\TransitionInterface|null $source
// *
// * @return TokenInterface
// */
// protected function activateNextState(ConnectionInterface $flow, ExecutionInstanceInterface $instance, CollectionInterface $consumeTokens, array $properties = [], TransitionInterface $source = null)
// {
// $nextState = $flow->targetState();
// $loop = $this->getOwner()->getLoopCharacteristics();
// if ($loop && $loop->isExecutable()) {
// $loop->iterateNextState($nextState, $instance, $consumeTokens, $properties, $source);
// } else {
// $nextState->addNewToken($instance, $properties, $source);
// }
// }
}
3 changes: 3 additions & 0 deletions src/ProcessMaker/Nayra/Bpmn/LoopCharacteristicsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ private function setLoopInstanceProperty(TokenInterface $token, $key, $value)
public function getLoopInstanceProperty(TokenInterface $token, $key, $defaultValue = null)
{
$loopCharacteristics = $token->getProperty(LoopCharacteristicsInterface::BPMN_LOOP_INSTANCE_PROPERTY, []);
if (!isset($loopCharacteristics['sourceToken'])) {
return $defaultValue;
}
$outerInstance = $loopCharacteristics['sourceToken'];
$ds = $token->getInstance()->getDataStore();
$data = $ds->getData(LoopCharacteristicsInterface::BPMN_LOOP_INSTANCE_PROPERTY, []);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,10 @@ public function continueLoop(ExecutionInstanceInterface $instance, TokenInterfac
*/
public function isLoopCompleted(ExecutionInstanceInterface $instance, TokenInterface $token)
{
$numberOfInstances = $this->getLoopInstanceProperty($token, 'numberOfInstances', 0);
$numberOfInstances = $this->getLoopInstanceProperty($token, 'numberOfInstances', null);
if ($numberOfInstances === null) {
$numberOfInstances = $this->calcNumberOfInstances($instance);
}
$completed = $this->getLoopInstanceProperty($token, 'numberOfCompletedInstances', 0);
return $completed >= $numberOfInstances;
}
Expand Down
3 changes: 2 additions & 1 deletion src/ProcessMaker/Nayra/Contracts/Bpmn/ActivityInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface ActivityInterface extends FlowNodeInterface
const EVENT_ACTIVITY_EXCEPTION = 'ActivityException';
const EVENT_ACTIVITY_CANCELLED = 'ActivityCancelled';
const EVENT_ACTIVITY_CLOSED = 'ActivityClosed';
const EVENT_EVENT_TRIGGERED = 'EventTriggered';
const EVENT_ACTIVITY_SKIPPED = 'ActivitySkipped';

/**
* Properties and composed elements
Expand All @@ -32,6 +32,7 @@ interface ActivityInterface extends FlowNodeInterface
const TOKEN_STATE_FAILING = 'FAILING';
const TOKEN_STATE_COMPLETED = 'COMPLETED';
const TOKEN_STATE_CLOSED = 'CLOSED';
const TOKEN_STATE_SKIPPED = 'SKIPPED';

/**
* Get Process of the activity.
Expand Down
150 changes: 150 additions & 0 deletions tests/Feature/Engine/MultiInstanceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -969,4 +969,154 @@ private function verifyStoredInstanceData(ExecutionInstanceInterface $instance)

$this->assertEquals($instanceData, $storageData);
}

/**
* Tests a process with MI with empty InputItems
*
* @return void
*/
public function testLoopWithEmptyInputItems()
{
// Load a BpmnFile Repository
$bpmnRepository = new BpmnDocument();
$bpmnRepository->setEngine($this->engine);
$bpmnRepository->setFactory($this->repository);
$bpmnRepository->load(__DIR__ . '/../Patterns/files/MultiInstance_EmptyInputItems.bpmn');

// Load a process from a bpmn repository by Id
$process = $bpmnRepository->getProcess('ProcessId');

// Get 'task_1' activity of the process
$taskOne = $bpmnRepository->getActivity('task_1');

// Get 'MultiInstanceTask' activity of the process
$miTask = $bpmnRepository->getActivity('task_2');

// Get 'last task' activity
$lastTask = $bpmnRepository->getActivity('task_3');

// Start the process with empty input items
$instance = $process->call();
$instance->getDataStore()->putData('items', []);
$this->engine->runToNextState();

// Assertion: A process has started.
$this->assertEquals(1, $process->getInstances()->count());

// Assertion: The process has started and the first activity was actived
$this->assertEvents([
ProcessInterface::EVENT_PROCESS_INSTANCE_CREATED,
EventInterface::EVENT_EVENT_TRIGGERED,
ActivityInterface::EVENT_ACTIVITY_ACTIVATED,
]);

// Complete the first activity.
$token = $taskOne->getTokens($instance)->item(0);
$taskOne->complete($token);
$this->engine->runToNextState();

// Assertion: The first activity was completed, the task of multiple instances skipped and continued to the last task.
$this->assertEvents([
ActivityInterface::EVENT_ACTIVITY_COMPLETED,
ActivityInterface::EVENT_ACTIVITY_CLOSED,

ActivityInterface::EVENT_ACTIVITY_SKIPPED,

ActivityInterface::EVENT_ACTIVITY_ACTIVATED,
]);

// Assertion: The internal data of the LoopCharacteristics are stored in the instance data.
$this->verifyStoredInstanceData($instance);

// Complete the last task
$token = $lastTask->getTokens($instance)->item(0);
$taskOne->complete($token);
$this->engine->runToNextState();

// Assertion: The last task was completed and process is completed
$this->assertEvents([
ActivityInterface::EVENT_ACTIVITY_COMPLETED,
ActivityInterface::EVENT_ACTIVITY_CLOSED,
EndEventInterface::EVENT_THROW_TOKEN_ARRIVES,
EndEventInterface::EVENT_THROW_TOKEN_CONSUMED,
EndEventInterface::EVENT_EVENT_TRIGGERED,

ProcessInterface::EVENT_PROCESS_INSTANCE_COMPLETED,
]);
}

/**
* Tests a process with MI with empty InputItems
*
* @return void
*/
public function testLoopWithCardinalityZero()
{
// Load a BpmnFile Repository
$bpmnRepository = new BpmnDocument();
$bpmnRepository->setEngine($this->engine);
$bpmnRepository->setFactory($this->repository);
$bpmnRepository->load(__DIR__ . '/../Patterns/files/MultiInstance_CardinalityZero.bpmn');

// Load a process from a bpmn repository by Id
$process = $bpmnRepository->getProcess('ProcessId');

// Get 'task_1' activity of the process
$taskOne = $bpmnRepository->getActivity('task_1');

// Get 'MultiInstanceTask' activity of the process
$miTask = $bpmnRepository->getActivity('task_2');

// Get 'last task' activity
$lastTask = $bpmnRepository->getActivity('task_3');

// Start the process with empty input items
$instance = $process->call();
$instance->getDataStore()->putData('times', 0);
$this->engine->runToNextState();

// Assertion: A process has started.
$this->assertEquals(1, $process->getInstances()->count());

// Assertion: The process has started and the first activity was actived
$this->assertEvents([
ProcessInterface::EVENT_PROCESS_INSTANCE_CREATED,
EventInterface::EVENT_EVENT_TRIGGERED,
ActivityInterface::EVENT_ACTIVITY_ACTIVATED,
]);

// Complete the first activity.
$token = $taskOne->getTokens($instance)->item(0);
$taskOne->complete($token);
$this->engine->runToNextState();

// Assertion: The first activity was completed, the task of multiple instances skipped and continued to the last task.
$this->assertEvents([
ActivityInterface::EVENT_ACTIVITY_COMPLETED,
ActivityInterface::EVENT_ACTIVITY_CLOSED,

ActivityInterface::EVENT_ACTIVITY_SKIPPED,

ActivityInterface::EVENT_ACTIVITY_ACTIVATED,
]);

// Assertion: The internal data of the LoopCharacteristics are stored in the instance data.
$this->verifyStoredInstanceData($instance);

// Complete the last task
$token = $lastTask->getTokens($instance)->item(0);
$taskOne->complete($token);
$this->engine->runToNextState();

// Assertion: The last task was completed and process is completed
$this->assertEvents([
ActivityInterface::EVENT_ACTIVITY_COMPLETED,
ActivityInterface::EVENT_ACTIVITY_CLOSED,
EndEventInterface::EVENT_THROW_TOKEN_ARRIVES,
EndEventInterface::EVENT_THROW_TOKEN_CONSUMED,
EndEventInterface::EVENT_EVENT_TRIGGERED,

ProcessInterface::EVENT_PROCESS_INSTANCE_COMPLETED,
]);
}
}
Loading

0 comments on commit 2edd797

Please sign in to comment.