diff --git a/.coveralls.yml b/.coveralls.yml
new file mode 100644
index 0000000..4eecff5
--- /dev/null
+++ b/.coveralls.yml
@@ -0,0 +1,3 @@
+service_name: travis-ci
+coverage_clover: clover.xml
+json_path: coveralls-upload.json
diff --git a/.docker/php7.3-dev/Dockerfile b/.docker/php7.3-dev/Dockerfile
new file mode 100644
index 0000000..fcc639c
--- /dev/null
+++ b/.docker/php7.3-dev/Dockerfile
@@ -0,0 +1,18 @@
+FROM php:7.3-cli
+
+RUN apt-get update && apt-get install -y git unzip
+
+ENV COMPOSER_ALLOW_SUPERUSER 1
+ENV COMPOSER_MEMORY_LIMIT -1
+
+RUN mkdir /.composer_cache
+ENV COMPOSER_CACHE_DIR /.composer_cache
+
+RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
+
+RUN composer -vvv global require hirak/prestissimo
+
+# php extensions
+
+RUN pecl install xdebug
+RUN docker-php-ext-enable xdebug
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..f5939b2
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,25 @@
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+
+# 4 space indentation
+[*.php]
+indent_style = space
+indent_size = 4
+
+# Tab indentation (no size specified)
+[Makefile]
+indent_style = tab
+
+# Matches the exact files either package.json or .travis.yml
+[{*.yml, *.yaml}]
+indent_style = space
+indent_size = 2
+
+[composer.json]
+indent_size = 4
diff --git a/.env b/.env
new file mode 100644
index 0000000..2ef5087
--- /dev/null
+++ b/.env
@@ -0,0 +1,13 @@
+# This file is a "template" of which env vars need to be defined for your application
+# Copy this file to .env file for development, create environment variables when deploying to production
+# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
+
+###> symfony/framework-bundle ###
+APP_ENV=dev
+APP_SECRET=secret
+###< symfony/framework-bundle ###
+
+###> common variables ###
+MICROTSK_COMPOSE_PROJECT_NAME=micro-task
+CI_COMMIT_REF_SLUG=master
+###< common variables ###
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d2f2a31
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,32 @@
+vendor/
+
+###> PhpStorm project profile ###
+.idea/
+###< PhpStorm project profile ###
+
+###> phpunit/phpunit ###
+phpunit.xml
+.phpunit.result.cache
+###< phpunit/phpunit ###
+
+###> friendsofphp/php-cs-fixer ###
+.php_cs.cache
+###< friendsofphp/php-cs-fixer ###
+
+###> squizlabs/php_codesniffer ###
+.phpcs-cache
+###< squizlabs/php_codesniffer ###
+
+###> sensiolabs-de/deptrac ###
+.deptrac.cache
+###< sensiolabs-de/deptrac ###
+
+# Build data
+build/
+
+###> Phpunit ###
+bin/.phpunit
+###< Phpunit ###
+
+composer.lock
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..270a07b
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,30 @@
+
+language: php
+
+matrix:
+ include:
+ - php: 7.3
+ fast_finish: true
+
+env:
+ global:
+ TEST_CONFIG="phpunit.xml.dist"
+
+before_install:
+ - echo "memory_limit=2G" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini || return 0
+
+install:
+ - travis_retry composer self-update
+ - composer install
+
+script:
+ - vendor/bin/phpstan analyse -l 6 -c phpstan.neon src tests
+ - vendor/bin/psalm --config=psalm.xml
+ - vendor/bin/ecs check src tests
+ - vendor/bin/phpmd src/ text phpmd.xml
+ - vendor/bin/phpunit --configuration $TEST_CONFIG
+ - composer validate --no-check-publish
+ - git log $(git describe --abbrev=0 --tags)...HEAD --no-merges --pretty=format:"* [%h](http://github.com/${TRAVIS_REPO_SLUG}/commit/%H) %s (%cN)"
+
+after_success:
+ - travis_retry php ./vendor/bin/php-coveralls -v --config .coveralls.yml -v;
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..328007c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,87 @@
+version = $(shell git describe --tags --dirty --always)
+build_name = application-$(version)
+# use the rest as arguments for "run"
+RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
+# ...and turn them into do-nothing targets
+#$(eval $(RUN_ARGS):;@:)
+
+.PHONY: fix-permission
+fix-permission: ## fix permission for docker env
+ sudo chown -R $(shell whoami):$(shell whoami) *
+ sudo chown -R $(shell whoami):$(shell whoami) .docker/*
+
+.PHONY: build
+build: ## build environment and initialize composer and project dependencies
+ docker-compose build
+ make composer-install
+
+.PHONY: stop
+stop:
+ docker-compose stop
+
+.PHONY: composer-install
+composer-install: ## Install project dependencies
+ docker-compose run --rm --no-deps php sh -lc 'composer install'
+
+.PHONY: composer-update
+composer-update: ## Update project dependencies
+ docker-compose run --rm --no-deps php sh -lc 'composer update'
+
+.PHONY: composer-outdated
+composer-outdated: ## Show outdated project dependencies
+ docker-compose run --rm --no-deps php sh -lc 'composer outdated'
+
+.PHONY: composer-validate
+composer-validate: ## Validate composer config
+ docker-compose run --rm --no-deps php sh -lc 'composer validate --no-check-publish'
+
+.PHONY: composer
+composer: ## Execute composer command
+ docker-compose run --rm --no-deps php sh -lc "composer $(RUN_ARGS)"
+
+.PHONY: phpunit
+phpunit: ## execute project unit tests
+ docker-compose run --rm php sh -lc "./vendor/bin/phpunit $(conf)"
+
+.PHONY: style
+style: ## executes php analizers
+ docker-compose run --rm --no-deps php sh -lc './vendor/bin/phpstan analyse -l 6 -c phpstan.neon src tests'
+ docker-compose run --rm --no-deps php sh -lc './vendor/bin/psalm --config=psalm.xml'
+
+.PHONY: lint
+lint: ## checks syntax of PHP files
+ docker-compose run --rm --no-deps php sh -lc './vendor/bin/parallel-lint ./ --exclude vendor --exclude bin/.phpunit'
+
+.PHONY: layer
+layer: ## Check issues with layers (deptrac tool)
+ docker-compose run --rm --no-deps php sh -lc './vendor/bin/deptrac analyze --formatter-graphviz=0'
+
+.PHONY: logs
+logs: ## look for service logs
+ docker-compose logs -f $(RUN_ARGS)
+
+.PHONY: help
+help: ## Display this help message
+ @cat $(MAKEFILE_LIST) | grep -e "^[a-zA-Z_\-]*: *.*## *" | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+
+.PHONY: php-shell
+php-shell: ## PHP shell
+ docker-compose run --rm php sh -l
+
+unit-tests: ## Run unit-tests suite
+ docker-compose run --rm php sh -lc 'vendor/bin/phpunit --testsuite unit-tests'
+
+static-analysis: style layer coding-standards ## Run phpstan, deprac, easycoding standarts code static analysis
+
+coding-standards: ## Run check and validate code standards tests
+ docker-compose run --rm --no-deps php sh -lc 'vendor/bin/ecs check src tests'
+ docker-compose run --rm --no-deps php sh -lc 'vendor/bin/phpmd src/ text phpmd.xml'
+
+coding-standards-fixer: ## Run code standards fixer
+ docker-compose run --rm --no-deps php sh -lc 'vendor/bin/ecs check src tests --fix'
+
+security-tests: ## The SensioLabs Security Checker
+ docker-compose run --rm --no-deps php sh -lc 'vendor/bin/security-checker security:check --end-point=http://security.sensiolabs.org/check_lock'
+
+.PHONY: test lint static-analysis phpunit coding-standards composer-validate
+test: build lint static-analysis phpunit coding-standards composer-validate stop ## Run all test suites
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..6452546
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,50 @@
+{
+ "name": "micro-module/task",
+ "type": "library",
+ "description": "Micro module Task common library",
+ "license": "proprietary",
+ "require": {
+ "php": "^7.3",
+ "beberlei/assert": "^3.2",
+ "broadway/broadway": "^2.2",
+ "queue-interop/amqp-interop": "^0.8",
+ "queue-interop/queue-interop": "^0.7|^0.8",
+ "enqueue/enqueue": "^0.10",
+ "enqueue/fs": "^0.10",
+ "enqueue/job-queue": "^0.10",
+ "enqueue/null": "^0.10",
+ "league/tactician-bundle": "^1.1",
+ "league/tactician-command-events": "^0.6.0",
+ "psr/log": "^1.1"
+ },
+ "require-dev": {
+ "php-parallel-lint/php-console-highlighter": "^0.4",
+ "php-parallel-lint/php-parallel-lint": "^1.0",
+ "mockery/mockery": "^1.3",
+ "phpmd/phpmd": "^2.8",
+ "phpstan/phpstan": "^0.12",
+ "phpstan/phpstan-mockery": "^0.12",
+ "phpstan/phpstan-phpunit": "^0.12",
+ "phpunit/phpunit": "^9.0",
+ "roave/security-advisories": "dev-master",
+ "sensiolabs-de/deptrac-shim": "^0.4",
+ "symplify/easy-coding-standard": "^7.2",
+ "vimeo/psalm": "^3.11"
+ },
+ "config": {
+ "preferred-install": {
+ "*": "dist"
+ },
+ "sort-packages": true
+ },
+ "autoload": {
+ "psr-4": {
+ "MicroModule\\Task\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "MicroModule\\Task\\Tests\\Unit\\": "tests/unit"
+ }
+ }
+}
diff --git a/depfile.yml b/depfile.yml
new file mode 100644
index 0000000..9cd398e
--- /dev/null
+++ b/depfile.yml
@@ -0,0 +1,25 @@
+paths:
+ - ./src
+exclude_files:
+
+layers:
+ - name: Domain
+ collectors:
+ - type: className
+ regex: .*\\Domain\\.*
+ - name: Application
+ collectors:
+ - type: className
+ regex: .*\\Application\\.*
+ - name: Infrastructure
+ collectors:
+ - type: className
+ regex: .*\\Infrastructure\\.*
+
+ruleset:
+ Domain:
+ Application:
+ - Domain
+ Infrastructure:
+ - Domain
+ - Application
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..42158bd
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,12 @@
+version: "3.7"
+
+services:
+ php:
+ container_name: ${MICROTSK_COMPOSE_PROJECT_NAME}_php
+ user: 1000:1000
+ build:
+ context: .docker/php7.3-dev
+ volumes:
+ - ~/.composer/cache/:/.composer_cache/:rw
+ - ..:/packages:rw
+ working_dir: /packages/Task
diff --git a/ecs.yml b/ecs.yml
new file mode 100644
index 0000000..847dc42
--- /dev/null
+++ b/ecs.yml
@@ -0,0 +1,4 @@
+imports:
+ - { resource: 'vendor/symplify/easy-coding-standard/config/clean-code.yml' }
+ - { resource: 'vendor/symplify/easy-coding-standard/config/symfony.yml' }
+ - { resource: 'vendor/symplify/easy-coding-standard/config/php71.yml' }
diff --git a/phpmd.xml b/phpmd.xml
new file mode 100644
index 0000000..700df16
--- /dev/null
+++ b/phpmd.xml
@@ -0,0 +1,28 @@
+
+
+ Ruleset for PHP Mess Detector that enforces coding standards
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..58f7090
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,7 @@
+includes:
+ - vendor/phpstan/phpstan-mockery/extension.neon
+ - vendor/phpstan/phpstan-phpunit/extension.neon
+
+parameters:
+ ignoreErrors:
+ - '#Parameter .* of .* expects .*, Mockery\\MockInterface given.#'
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..4539d9e
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tests/unit
+
+
+
+
+
+
+
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..13b78a0
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Application/EventListener/JobCommandBusEventListenerInterface.php b/src/Application/EventListener/JobCommandBusEventListenerInterface.php
new file mode 100644
index 0000000..9269148
--- /dev/null
+++ b/src/Application/EventListener/JobCommandBusEventListenerInterface.php
@@ -0,0 +1,52 @@
+ self::METHOD_PRE_PROCESS,
+ self::EVENT_POST_PROCESS => self::METHOD_POST_PROCESS,
+ self::EVENT_FAILED_PROCESS => self::METHOD_FAILED_PROCESS,
+ ];
+
+ /**
+ * Job pre process command event action.
+ *
+ * @param Message $messagee
+ * @param CommandInterface $command
+ */
+ public function preProcessCommand(Message $messagee, CommandInterface $command): void;
+
+ /**
+ * Job post process command event action.
+ *
+ * @param Message $message
+ * @param CommandInterface $command
+ */
+ public function postProcessCommand(Message $message, CommandInterface $command): void;
+
+ /**
+ * Job failed process command event action.
+ *
+ * @param Message $message
+ * @param CommandInterface $command
+ */
+ public function failedProcessCommand(Message $message, CommandInterface $command): void;
+}
diff --git a/src/Application/Processor/JobCommandBusProcessor.php b/src/Application/Processor/JobCommandBusProcessor.php
new file mode 100644
index 0000000..03c0694
--- /dev/null
+++ b/src/Application/Processor/JobCommandBusProcessor.php
@@ -0,0 +1,222 @@
+jobRunner = $jobRunner;
+ $this->commandBus = $commandBus;
+ $this->commandFactory = $commandFactory;
+ $this->eventDispatcher = $eventDispatcher;
+ }
+
+ /**
+ * Process enqueue message.
+ *
+ * @param Message $message
+ * @param Context $context
+ *
+ * @return object|string
+ *
+ * @throws Throwable
+ */
+ public function process(Message $message, Context $context)
+ {
+ /** @var CommandInterface $command */
+ [$type, $command] = $this->makeCommand($message);
+ // Build unique job name
+ $name = $type.'_'.$command->getUuid()->toString();
+ $ownerId = $message->getMessageId() ?? $command->getUuid()->toString();
+
+ $this->eventDispatcher->dispatch(
+ JobCommandBusEventListenerInterface::EVENT_PRE_PROCESS,
+ [$message, $command]
+ );
+
+ $this->jobRunner->runUnique(
+ $ownerId,
+ $name,
+ $this->getTaskCallback($type, $command)
+ );
+
+ if (!$this->isFinishSuccessfully()) {
+ $this->eventDispatcher->dispatch(
+ JobCommandBusEventListenerInterface::EVENT_FAILED_PROCESS,
+ [$message, $command]
+ );
+
+ return self::REJECT;
+ }
+
+ $this->eventDispatcher->dispatch(
+ JobCommandBusEventListenerInterface::EVENT_POST_PROCESS,
+ [$message, $command]
+ );
+
+ return self::ACK;
+ }
+
+ /**
+ * Build and return task callback.
+ *
+ * @param string $type
+ * @param CommandInterface $command
+ *
+ * @return Closure
+ */
+ private function getTaskCallback(string $type, CommandInterface $command): Closure
+ {
+ return function (JobRunner $runner, Job $job) use ($command): bool {
+ try {
+ $this->commandBus->handle($command);
+ $this->setResult(true);
+
+ return true;
+ } catch (Throwable $e) {
+ $this->logMessage($e->getMessage(), LOG_INFO);
+ $this->setResult(false);
+
+ return false;
+ }
+ };
+ }
+
+ /**
+ * Is last job finish successfully.
+ *
+ * @return bool
+ */
+ private function isFinishSuccessfully(): bool
+ {
+ return $this->jobResult;
+ }
+
+ /**
+ * Set job task final boolean result.
+ *
+ * @param bool $result
+ *
+ * @return $this
+ */
+ private function setResult(bool $result): self
+ {
+ $this->jobResult = $result;
+
+ return $this;
+ }
+
+ /**
+ * Make CommandBus command.
+ *
+ * @param Message $message
+ *
+ * @return mixed[]
+ *
+ * @throws AssertionFailedException
+ */
+ private function makeCommand(Message $message): array
+ {
+ $data = JSON::decode($message->getBody());
+ Assertion::keyExists($data, 'type');
+ Assertion::keyExists($data, 'args');
+ $commandType = $data['type'];
+ $args = $data['args'];
+
+ if (!is_array($args)) {
+ $args = [$args];
+ }
+ /** @psalm-suppress TooManyArguments */
+ $command = $this->commandFactory->makeCommandInstanceByType($commandType, ...$args);
+
+ return [$commandType, $command];
+ }
+
+ /**
+ * Return enqueue command routers.
+ *
+ * @return string
+ */
+ public static function getSubscribedCommand(): string
+ {
+ return static::getRoute();
+ }
+
+ /**
+ * Return enqueue route.
+ *
+ * @return string
+ */
+ public static function getRoute(): string
+ {
+ return 'task.command.run';
+ }
+}
diff --git a/src/Application/Processor/JobConsumerInterface.php b/src/Application/Processor/JobConsumerInterface.php
new file mode 100644
index 0000000..09efa11
--- /dev/null
+++ b/src/Application/Processor/JobConsumerInterface.php
@@ -0,0 +1,16 @@
+getBody();
+ $this->logMessage('Consume job event', LOG_DEBUG);
+
+ try {
+ $messageBody = json_decode($messageBody, true);
+ } catch (Throwable $exception) {
+ $this->logMessage('Consume job event with Exception', LOG_DEBUG);
+ }
+
+ return self::ACK;
+ }
+
+ /**
+ * Return enqueue command routers.
+ *
+ * @return string
+ */
+ public static function getSubscribedTopics(): string
+ {
+ return self::SUBSCRIBED_TASK_EVENT_COMMAND;
+ }
+}
diff --git a/src/Application/Processor/JobProcessor.php b/src/Application/Processor/JobProcessor.php
new file mode 100644
index 0000000..8988016
--- /dev/null
+++ b/src/Application/Processor/JobProcessor.php
@@ -0,0 +1,125 @@
+taskEventProducer = $taskEventProducer;
+ }
+
+ /**
+ * @param Job $job
+ *
+ * @throws LoggerException
+ */
+ public function startChildJob(Job $job): void
+ {
+ parent::startChildJob($job); // TODO: Change the autogenerated stub
+
+ [$name, $uuid] = explode('_', $job->getName());
+ $this->logMessage(sprintf('Start job `%s`, uuid `%s`, jobId `%s`', $name, $uuid, $job->getId()), LOG_INFO);
+ $this->taskEventProducer->sendEvent(JobEventProcessor::SUBSCRIBED_TASK_EVENT_COMMAND,
+ [
+ 'name' => $name,
+ 'uuid' => $uuid,
+ 'id' => $job->getId(),
+ 'status' => self::JOB_TASK_STATUS_STARTED,
+ ]);
+ }
+
+ /**
+ * @param Job $job
+ *
+ * @throws LoggerException
+ */
+ public function successChildJob(Job $job): void
+ {
+ parent::successChildJob($job); // TODO: Change the autogenerated stub
+ [$name, $uuid] = explode('_', $job->getName());
+ $this->logMessage(sprintf('Finish successfully job `%s`, uuid `%s`, jobId `%s`', $name, $uuid, $job->getId()), LOG_INFO);
+ $this->taskEventProducer->sendEvent(JobEventProcessor::SUBSCRIBED_TASK_EVENT_COMMAND,
+ [
+ 'name' => $name,
+ 'uuid' => $uuid,
+ 'id' => $job->getId(),
+ 'status' => self::JOB_TASK_STATUS_SUCCESS,
+ ]);
+ }
+
+ /**
+ * @param Job $job
+ *
+ * @throws LoggerException
+ */
+ public function failChildJob(Job $job): void
+ {
+ parent::failChildJob($job); // TODO: Change the autogenerated stub
+ [$name, $uuid] = explode('_', $job->getName());
+ $this->logMessage(sprintf('Finish unsuccessfully job `%s`, uuid `%s`, jobId `%s`', $name, $uuid, $job->getId()), LOG_INFO);
+ $this->taskEventProducer->sendEvent(JobEventProcessor::SUBSCRIBED_TASK_EVENT_COMMAND,
+ [
+ 'name' => $name,
+ 'uuid' => $uuid,
+ 'id' => $job->getId(),
+ 'status' => self::JOB_TASK_STATUS_FAILED,
+ ]);
+ }
+
+ /**
+ * @param Job $job
+ *
+ * @throws LoggerException
+ */
+ public function cancelChildJob(Job $job): void
+ {
+ parent::cancelChildJob($job); // TODO: Change the autogenerated stub
+ [$name, $uuid] = explode('_', $job->getName());
+ $this->logMessage(sprintf('Canceled job `%s`, uuid `%s`, jobId `%s`', $name, $uuid, $job->getId()), LOG_INFO);
+ $this->taskEventProducer->sendEvent(JobEventProcessor::SUBSCRIBED_TASK_EVENT_COMMAND,
+ [
+ 'name' => $name,
+ 'uuid' => $uuid,
+ 'id' => $job->getId(),
+ 'status' => self::JOB_TASK_STATUS_CANCELED,
+ ]);
+ }
+
+ protected function sendCalculateRootJobStatusEvent(Job $job): void
+ {
+ // remove send jobId to queue
+ }
+}
diff --git a/src/Infrastructure/Service/Test/JobRunner.php b/src/Infrastructure/Service/Test/JobRunner.php
new file mode 100644
index 0000000..eb85ca8
--- /dev/null
+++ b/src/Infrastructure/Service/Test/JobRunner.php
@@ -0,0 +1,103 @@
+runUniqueJobs[] = ['ownerId' => $ownerId, 'jobName' => $jobName, 'runCallback' => $runCallback];
+ $job = new Job();
+ $job->setId(random_int(1, 100));
+
+ return $runCallback($this, $job);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return mixed
+ *
+ * @throws Exception
+ */
+ public function createDelayed($jobName, callable $startCallback)
+ {
+ $this->createDelayedJobs[] = ['jobName' => $jobName, 'runCallback' => $startCallback];
+ $job = new Job();
+ $job->setId(random_int(1, 100));
+
+ return $startCallback($this, $job);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param string $jobId
+ * @param callable $runCallback
+ *
+ * @return mixed
+ *
+ * @throws Exception
+ */
+ public function runDelayed($jobId, callable $runCallback)
+ {
+ $this->runDelayedJobs[] = ['jobId' => $jobId, 'runCallback' => $runCallback];
+ $job = new Job();
+ $job->setId(random_int(1, 100));
+
+ return $runCallback($this, $job);
+ }
+
+ /**
+ * @return mixed[]
+ */
+ public function getRunUniqueJobs(): array
+ {
+ return $this->runUniqueJobs;
+ }
+
+ /**
+ * @return mixed[]
+ */
+ public function getCreateDelayedJobs(): array
+ {
+ return $this->createDelayedJobs;
+ }
+
+ /**
+ * @return mixed[]
+ */
+ public function getRunDelayedJobs(): array
+ {
+ return $this->runDelayedJobs;
+ }
+}
diff --git a/tests/unit/Application/Processor/JobCommandBusProcessorTest.php b/tests/unit/Application/Processor/JobCommandBusProcessorTest.php
new file mode 100644
index 0000000..e236ae5
--- /dev/null
+++ b/tests/unit/Application/Processor/JobCommandBusProcessorTest.php
@@ -0,0 +1,263 @@
+makeCommandBusMock(1, false);
+
+ $uuidMock = $this->makeUuidMock($uuid, 1);
+ $commandMock = $this->makeCommandMock($uuidMock, 1);
+ $commandFactoryMock = $this->makeCommandFactoryMock($commandMock);
+ $this->traceableEventDispatcher = new TraceableEventDispatcher();
+
+ $jobProgramConsumer = new JobCommandBusProcessor($testJobRunner, $commandBusMock, $commandFactoryMock, $this->traceableEventDispatcher);
+ self::assertInstanceOf(Processor::class, $jobProgramConsumer);
+ self::assertInstanceOf(CommandSubscriberInterface::class, $jobProgramConsumer);
+
+ $loggerMock = $this->makeLoggerMock(0);
+ $jobProgramConsumer->setLogger($loggerMock);
+ $messageMock = $this->makeMessageMock($uuid, $taskCommand, 3);
+ /** @var Context $contextMock */
+ $contextMock = Mockery::mock(Context::class);
+
+ self::assertSame(Processor::ACK, $jobProgramConsumer->process($messageMock, $contextMock));
+
+ $dispatchedEvents = $this->traceableEventDispatcher->getDispatchedEvents();
+ $this->assertCount(2, $dispatchedEvents);
+ $this->assertEquals('enqueue.job.pre_process', $dispatchedEvents[0]['event']);
+ $this->assertInstanceOf(Message::class, $dispatchedEvents[0]['arguments'][0]);
+ $this->assertInstanceOf(CommandInterface::class, $dispatchedEvents[0]['arguments'][1]);
+ $this->assertEquals($uuid, $dispatchedEvents[0]['arguments'][0]->getMessageId());
+ $this->assertEquals($taskCommand, $dispatchedEvents[0]['arguments'][0]->getBody());
+
+ $this->assertEquals('enqueue.job.post_process', $dispatchedEvents[1]['event']);
+ $this->assertInstanceOf(Message::class, $dispatchedEvents[0]['arguments'][0]);
+ $this->assertInstanceOf(CommandInterface::class, $dispatchedEvents[0]['arguments'][1]);
+ $this->assertEquals($uuid, $dispatchedEvents[0]['arguments'][0]->getMessageId());
+ $this->assertEquals($taskCommand, $dispatchedEvents[0]['arguments'][0]->getBody());
+ }
+
+ /**
+ * @test
+ *
+ * @group unit
+ *
+ * @dataProvider \MicroModule\Task\Tests\Unit\DataProvider\TaskDataProvider::getData
+ *
+ * @param string $taskCommand
+ * @param string $uuid
+ *
+ * @throws Throwable
+ */
+ public function processFailedTest(string $taskCommand, string $uuid): void
+ {
+ $testJobRunner = new JobRunner(Mockery::mock(\Enqueue\JobQueue\JobProcessor::class));
+ /** @var CommandBus $commandBusMock */
+ $commandBusMock = $this->makeCommandBusMock(1, true);
+
+ $uuidMock = $this->makeUuidMock($uuid, 1);
+ $commandMock = $this->makeCommandMock($uuidMock, 1);
+ $commandFactoryMock = $this->makeCommandFactoryMock($commandMock);
+ $this->traceableEventDispatcher = new TraceableEventDispatcher();
+
+ $jobProgramConsumer = new JobCommandBusProcessor($testJobRunner, $commandBusMock, $commandFactoryMock, $this->traceableEventDispatcher);
+ self::assertInstanceOf(Processor::class, $jobProgramConsumer);
+ self::assertInstanceOf(CommandSubscriberInterface::class, $jobProgramConsumer);
+
+ $loggerMock = $this->makeLoggerMock(1);
+ $jobProgramConsumer->setLogger($loggerMock);
+ $messageMock = $this->makeMessageMock($uuid, $taskCommand, 3);
+ /** @var Context $contextMock */
+ $contextMock = Mockery::mock(Context::class);
+
+ self::assertSame(Processor::REJECT, $jobProgramConsumer->process($messageMock, $contextMock));
+
+ $dispatchedEvents = $this->traceableEventDispatcher->getDispatchedEvents();
+ $this->assertCount(2, $dispatchedEvents);
+
+ $this->assertEquals('enqueue.job.pre_process', $dispatchedEvents[0]['event']);
+ $this->assertInstanceOf(Message::class, $dispatchedEvents[0]['arguments'][0]);
+ $this->assertInstanceOf(CommandInterface::class, $dispatchedEvents[0]['arguments'][1]);
+ $this->assertEquals($uuid, $dispatchedEvents[0]['arguments'][0]->getMessageId());
+ $this->assertEquals($taskCommand, $dispatchedEvents[0]['arguments'][0]->getBody());
+
+ $this->assertEquals('enqueue.job.failed_process', $dispatchedEvents[1]['event']);
+ $this->assertInstanceOf(Message::class, $dispatchedEvents[0]['arguments'][0]);
+ $this->assertInstanceOf(CommandInterface::class, $dispatchedEvents[0]['arguments'][1]);
+ $this->assertEquals($uuid, $dispatchedEvents[0]['arguments'][0]->getMessageId());
+ $this->assertEquals($taskCommand, $dispatchedEvents[0]['arguments'][0]->getBody());
+ }
+
+ /**
+ * Return Logger mock object.
+ *
+ * @param int $times
+ * @param bool $throwException
+ *
+ * @return MockInterface
+ */
+ protected function makeCommandBusMock(int $times = 1, bool $throwException = false): MockInterface
+ {
+ $commandBusMock = Mockery::mock(CommandBus::class);
+ $handleMethod = $commandBusMock
+ ->shouldReceive('handle')
+ ->times($times)
+ ->andReturn('');
+
+ if ($throwException) {
+ $handleMethod->andThrow(Mockery\Exception::class, 'Test exception');
+ }
+
+ return $commandBusMock;
+ }
+
+ /**
+ * Make and return uuid mock.
+ *
+ * @param string $uuid
+ * @param int $times
+ *
+ * @return MockInterface
+ */
+ private function makeUuidMock(string $uuid, int $times = 1): MockInterface
+ {
+ $uuidMock = Mockery::mock(UuidInterface::class);
+ $uuidMock
+ ->shouldReceive('toString')
+ ->times($times)
+ ->andReturn($uuid);
+
+ return $uuidMock;
+ }
+
+ /**
+ * Make and return Command mock.
+ *
+ * @param MockInterface $uuidMock
+ * @param int $times
+ *
+ * @return MockInterface
+ */
+ private function makeCommandMock(MockInterface $uuidMock, int $times = 1): MockInterface
+ {
+ $commandMock = Mockery::mock(CommandInterface::class);
+ $commandMock
+ ->shouldReceive('getUuid')
+ ->times($times)
+ ->andReturn($uuidMock);
+
+ return $commandMock;
+ }
+
+ /**
+ * Make and return CommandFactory mock.
+ *
+ * @param MockInterface $commandMock
+ * @param int $times
+ *
+ * @return MockInterface
+ */
+ private function makeCommandFactoryMock(MockInterface $commandMock, int $times = 1): MockInterface
+ {
+ $commandFactoryMock = Mockery::mock(CommandFactoryInterface::class);
+ $commandFactoryMock
+ ->shouldReceive('makeCommandInstanceByType')
+ ->times($times)
+ ->andReturn($commandMock);
+
+ return $commandFactoryMock;
+ }
+
+ /**
+ * Return Logger mock object.
+ *
+ * @param int $times
+ *
+ * @return MockInterface
+ */
+ protected function makeLoggerMock(int $times = 1): MockInterface
+ {
+ $loggerMock = Mockery::mock(LoggerInterface::class);
+ $loggerMock
+ ->shouldReceive('info')
+ ->times($times)
+ ->andReturn('');
+
+ return $loggerMock;
+ }
+
+ /**
+ * Make and return Message mock.
+ *
+ * @param string $uuid
+ * @param string $taskCommand
+ * @param int $times
+ *
+ * @return MockInterface
+ */
+ private function makeMessageMock(string $uuid, string $taskCommand, int $times = 1): MockInterface
+ {
+ $messageMock = Mockery::mock(Message::class);
+ $messageMock
+ ->shouldReceive('getMessageId')
+ ->times($times)
+ ->andReturn($uuid);
+ $messageMock
+ ->shouldReceive('getBody')
+ ->times($times)
+ ->andReturn($taskCommand);
+
+ return $messageMock;
+ }
+}
diff --git a/tests/unit/DataProvider/TaskDataProvider.php b/tests/unit/DataProvider/TaskDataProvider.php
new file mode 100644
index 0000000..9c06be4
--- /dev/null
+++ b/tests/unit/DataProvider/TaskDataProvider.php
@@ -0,0 +1,30 @@
+ 'ProgramCollectionRunCommand', 'args' => ['72a541ba-4bb4-454f-9ed5-3dcfe6ca9f2e']]),
+ '72a541ba-4bb4-454f-9ed5-3dcfe6ca9f2e',
+ ],
+ ];
+ }
+}