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', + ], + ]; + } +}