diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..fe1ae53 --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,38 @@ +name: PHPUnit Tests + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Build the Docker image + run: make build + + - name: Install dependencies + run: make install + + - name: Run test suite + run: make test \ No newline at end of file diff --git a/.gitignore b/.gitignore index c76a9ff..3f58acd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.php_cs.cache +.*.cache /.idea/ /vendor/ composer.lock diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..b33e48c --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,25 @@ +setRiskyAllowed(true) + ->setRules([ + '@PSR12' => true, + '@Symfony' => true, + 'array_indentation' => true, + 'array_syntax' => ['syntax' => 'short'], + 'concat_space' => ['spacing' => 'one'], + 'declare_strict_types' => true, + 'string_implicit_backslashes' => true, + 'list_syntax' => ['syntax' => 'short'], + 'multiline_whitespace_before_semicolons' => ['strategy' => 'new_line_for_chained_calls'], + 'ordered_imports' => true, + 'phpdoc_to_comment' => false, + 'trailing_comma_in_multiline' => ['elements' => ['arrays', 'arguments', 'parameters']], + 'visibility_required' => ['elements' => ['property', 'method', 'const']], + ]) + ->setFinder( + PhpCsFixer\Finder::create() + ->in(__DIR__ . '/src') + ->in(__DIR__ . '/tests') + ) +; diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..40d0ebe --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM php:8.4-cli + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + unzip \ + libssl-dev \ + libcurl4-openssl-dev \ + pkg-config \ + openjdk-17-jre-headless \ + && rm -rf /var/lib/apt/lists/* + +# Install MongoDB extension +RUN pecl install mongodb \ + && docker-php-ext-enable mongodb \ + && docker-php-ext-install pcntl + +# Copy Composer from official image +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Set working directory +WORKDIR /app + +# Copy composer files +COPY composer.json composer.lock* ./ + +# Install dependencies including dev dependencies for testing +RUN composer install --optimize-autoloader + +# Copy application code +COPY . . + +# Set environment variable for Composer +ENV COMPOSER_ALLOW_SUPERUSER=1 + +CMD ["tail", "-f", "/dev/null"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..30a5db2 --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +.PHONY: build up down test phpstan fix-cs install update shell logs clean + +# Build the Docker image +build: + docker compose build + +# Start the services +up: + docker compose up -d + +# Stop the services +down: + docker compose down + +# Install dependencies +install: + docker compose run --rm php composer install + +# Update dependencies +update: + docker compose run --rm php composer update + +# Run all tests +test: up + docker compose exec php vendor/bin/phpunit + +phpstan: up + docker compose exec php vendor/bin/phpstan + +fix-cs: up + docker compose exec php vendor/bin/php-cs-fixer fix -v + +# Open a shell in the PHP container +shell: + docker compose exec php bash + +# View logs +logs: + docker compose logs -f php + +# Clean up containers and volumes +clean: + docker compose down -v + docker compose rm -f diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..5509e13 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,8 @@ +services: + php: + build: . + working_dir: /app + volumes: + - .:/app + environment: + - COMPOSER_ALLOW_SUPERUSER=1 diff --git a/composer.json b/composer.json index bf99c35..556a02a 100644 --- a/composer.json +++ b/composer.json @@ -1,31 +1,35 @@ { "name": "recruiterphp/geezer", "description": "PHP tools to build robust long-running processes", + "license": "MIT", "type": "library", - "license": "proprietary", "authors": [ { "name": "Easy Welfare Development Team", "email": "dev@easywelfare.com" + }, + { + "name": "Contributors", + "homepage": "https://github.com/recruiterphp/geezer/graphs/contributors" } ], "require": { - "php": "^7.2", + "php": "^8.4", "ext-pcntl": "*", - "symfony/console": "~4.0", - "recruiterphp/concurrency": "^3.0.0" + "recruiterphp/concurrency": "^4.0", + "symfony/console": "^7.3" }, "require-dev": { - "phpunit/phpunit": "^7", - "friendsofphp/php-cs-fixer": "^2.13", - "phpstan/phpstan": "^0.10.5", - "phpstan/phpstan-phpunit": "^0.10.0" + "ergebnis/composer-normalize": "^2.47", + "friendsofphp/php-cs-fixer": "^3.85", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^12.3", + "rector/rector": "^2.1" }, "provide": { - "easywelfare/geezer-implementation": "2.0.0" + "easywelfare/geezer-implementation": "self.version" }, - "minimum-stability": "dev", - "prefer-stable": true, + "minimum-stability": "stable", "autoload": { "psr-4": { "Recruiter\\Geezer\\": "src", @@ -36,5 +40,11 @@ "psr-4": { "Recruiter\\Geezer\\": "tests" } + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true, + "sort-packages": true + } } } diff --git a/phpstan.neon b/phpstan.neon index cc85d47..130e9ca 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,8 +1,5 @@ -includes: - - ./vendor/phpstan/phpstan-phpunit/extension.neon - - ./vendor/phpstan/phpstan-phpunit/rules.neon - parameters: - level: 7 - excludes_analyse: - - %currentWorkingDirectory%/vendor/ + level: 10 + paths: + - src + - tests diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..296ff04 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,19 @@ + + + + + tests + + + + + src + + + diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..9400286 --- /dev/null +++ b/rector.php @@ -0,0 +1,16 @@ +withPaths([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + // uncomment to reach your current PHP version + // ->withPhpSets() + ->withTypeCoverageLevel(0) + ->withDeadCodeLevel(0) + ->withCodeQualityLevel(0); diff --git a/src/Command/LeadershipEventsHandler.php b/src/Command/LeadershipEventsHandler.php index fbcb4b9..8b65124 100644 --- a/src/Command/LeadershipEventsHandler.php +++ b/src/Command/LeadershipEventsHandler.php @@ -1,5 +1,7 @@ name()); - $this->wrapped = $wrapped; - $this->logger = $logger; $this->setDescription($wrapped->description()); $this->setDefinition($wrapped->definition()); $this->leadershipStatus = null; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $this->wrapped->init($input); $this->registerSignalHandlers(); @@ -77,7 +63,7 @@ protected function execute(InputInterface $input, OutputInterface $output) try { $success = $this->wrapped->execute(); - } catch (Throwable $e) { + } catch (\Throwable $e) { $this->log('Thrown an Exception: ' . get_class($e), ['exception' => $e], LogLevel::ERROR); $occuredException = $e; @@ -102,21 +88,23 @@ protected function execute(InputInterface $input, OutputInterface $output) $leadershipStrategy->release(); if ($occuredException) { - exit(2); + return self::FAILURE; } + + return self::SUCCESS; } - private function leadershipWasAcquired() + private function leadershipWasAcquired(): void { $this->leadershipStatusChanged(self::LEADERSHIP_STATUS_ACQUIRED, 'leadershipAcquired'); } - private function leadershipWasLost() + private function leadershipWasLost(): void { $this->leadershipStatusChanged(self::LEADERSHIP_STATUS_LOST, 'leadershipLost'); } - private function leadershipStatusChanged($newLeadershipStatus, string $hook) + private function leadershipStatusChanged(?string $newLeadershipStatus, string $hook): void { if ($this->leadershipStatus === $newLeadershipStatus) { return; @@ -134,13 +122,13 @@ private function leadershipStatusChanged($newLeadershipStatus, string $hook) private function registerSignalHandlers(): void { foreach (self::STOP_SIGNALS as $signal) { - pcntl_signal($signal, function () { + pcntl_signal($signal, function (): void { $this->askedToStop = true; }); } } - private function askedToStop() + private function askedToStop(): bool { if (!$this->askedToStop) { pcntl_signal_dispatch(); @@ -152,7 +140,7 @@ private function askedToStop() private function sleepIfNotAskedToStop(int $milliSeconds): bool { $microSeconds = $milliSeconds * 1000; - for ($i = 0; $i < $microSeconds && !$this->askedToStop(); $i = $i + 50000) { + for ($i = 0; $i < $microSeconds && !$this->askedToStop(); $i += 50000) { usleep(50000); } @@ -165,15 +153,16 @@ private function sleepAccordingTo(WaitStrategy $waitStrategy): void $waitStrategy->next(); } - private function possiblyCollectGarbage(): int + private function possiblyCollectGarbage(): void { if (0 === (++$this->garbageCollectorCounter % self::CYCLES_BEFORE_GC)) { - return gc_collect_cycles(); + gc_collect_cycles(); } - - return 0; } + /** + * @param array $extra + */ private function log(string $message, array $extra = [], string $level = LogLevel::DEBUG): void { $this->logger->log($level, "[{hostname}:{pid}] $message", array_merge([ diff --git a/src/Leadership/Anarchy.php b/src/Leadership/Anarchy.php index aa5b5cc..b794cf5 100644 --- a/src/Leadership/Anarchy.php +++ b/src/Leadership/Anarchy.php @@ -1,5 +1,7 @@ attempt; } diff --git a/src/Timing/ExponentialBackoffStrategy.php b/src/Timing/ExponentialBackoffStrategy.php index 1e3f2c5..4f4c54e 100644 --- a/src/Timing/ExponentialBackoffStrategy.php +++ b/src/Timing/ExponentialBackoffStrategy.php @@ -1,23 +1,12 @@ from = $from; - $this->to = $to; } /** * Number of milliseconds to wait. - * - * @return int */ public function current(): int { @@ -44,7 +29,7 @@ public function current(): int return intval(min( 2 ** ($this->attempt - 1) * $this->from, - $this->to + $this->to, )); } @@ -52,9 +37,6 @@ public function current(): int * Return the key of the current element. * * @see https://php.net/manual/en/iterator.key.php - * - * @return int - * * @since 5.0.0 */ public function key(): int @@ -94,7 +76,7 @@ public function rewind(): void * @see https://php.net/manual/en/iterator.next.php * @since 5.0.0 */ - public function next() + public function next(): void { ++$this->attempt; } diff --git a/src/Timing/WaitStrategy.php b/src/Timing/WaitStrategy.php index 252669e..92bfd44 100644 --- a/src/Timing/WaitStrategy.php +++ b/src/Timing/WaitStrategy.php @@ -1,15 +1,16 @@ + */ +interface WaitStrategy extends \Iterator { /** * Number of milliseconds to wait. - * - * @return int */ public function current(): int; } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 7a9986c..e2741fe 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,5 +1,7 @@ assertEquals(0, $strategy->current());