From 45d7c93ed5416fb9155e769f4e04c8efbaeace40 Mon Sep 17 00:00:00 2001 From: Artem Onyshchenko Date: Fri, 24 Apr 2020 19:17:09 +0300 Subject: [PATCH] feat(master): Add Task module - Add Task module --- .coveralls.yml | 3 + .docker/php7.3-dev/Dockerfile | 18 ++ .editorconfig | 25 ++ .env | 13 + .gitignore | 32 +++ .travis.yml | 30 ++ Makefile | 87 ++++++ composer.json | 50 ++++ depfile.yml | 25 ++ docker-compose.yml | 12 + ecs.yml | 4 + phpmd.xml | 28 ++ phpstan.neon | 7 + phpunit.xml.dist | 27 ++ psalm.xml | 53 ++++ .../JobCommandBusEventListenerInterface.php | 52 ++++ .../Processor/JobCommandBusProcessor.php | 222 +++++++++++++++ .../Processor/JobConsumerInterface.php | 16 ++ .../Processor/JobEventProcessor.php | 59 ++++ src/Application/Processor/JobProcessor.php | 125 +++++++++ src/Infrastructure/Service/Test/JobRunner.php | 103 +++++++ .../Processor/JobCommandBusProcessorTest.php | 263 ++++++++++++++++++ tests/unit/DataProvider/TaskDataProvider.php | 30 ++ 23 files changed, 1284 insertions(+) create mode 100644 .coveralls.yml create mode 100644 .docker/php7.3-dev/Dockerfile create mode 100644 .editorconfig create mode 100644 .env create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Makefile create mode 100644 composer.json create mode 100644 depfile.yml create mode 100644 docker-compose.yml create mode 100644 ecs.yml create mode 100644 phpmd.xml create mode 100644 phpstan.neon create mode 100644 phpunit.xml.dist create mode 100644 psalm.xml create mode 100644 src/Application/EventListener/JobCommandBusEventListenerInterface.php create mode 100644 src/Application/Processor/JobCommandBusProcessor.php create mode 100644 src/Application/Processor/JobConsumerInterface.php create mode 100644 src/Application/Processor/JobEventProcessor.php create mode 100644 src/Application/Processor/JobProcessor.php create mode 100644 src/Infrastructure/Service/Test/JobRunner.php create mode 100644 tests/unit/Application/Processor/JobCommandBusProcessorTest.php create mode 100644 tests/unit/DataProvider/TaskDataProvider.php 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', + ], + ]; + } +}