diff --git a/.cirrus.yml b/.cirrus.yml index 2a3a7c07e..02e196b6a 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -14,6 +14,8 @@ BUILD_TEST_TASK_TEMPLATE: &BUILD_TEST_TASK_TEMPLATE - composer run lint static_analysis_script: - composer run static-code-analysis + generate_library_script: + - composer gen-lib test_script: - composer test @@ -26,12 +28,15 @@ linux_arm64_task: - VERSION: 8.0 arm_container: image: php:$VERSION + cpu: 4 + memory: 12G pre_req_script: - - apt update --yes && apt install --yes zip unzip git libffi-dev + - apt update --yes && apt install --yes zip unzip git libffi-dev protobuf-compiler - curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php - php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer - docker-php-ext-install sockets - docker-php-ext-install ffi + - MAKEFLAGS=" -j4" pecl install grpc version_check_script: - php --version << : *BUILD_TEST_TASK_TEMPLATE @@ -46,7 +51,8 @@ macos_arm64_task: macos_instance: image: ghcr.io/cirruslabs/macos-ventura-base:latest pre_req_script: - - brew install php@$VERSION composer + - brew install php@$VERSION composer protobuf + - MAKEFLAGS=" -j4" pecl install grpc version_check_script: - php --version << : *BUILD_TEST_TASK_TEMPLATE diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a7ebeac3f..599bf3397 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,15 +60,33 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - extensions: sockets, curl, zip, ffi + extensions: ${{ matrix.operating-system == 'windows-latest' && matrix.php == '8.2' && 'sockets, curl, zip, ffi' || 'sockets, curl, zip, ffi, grpc' }} php-version: ${{ matrix.php }} coverage: none ini-values: ${{ matrix.operating-system == 'windows-latest' && 'opcache.enable=0 opcache.enable_cli=0' || '' }} + - name: Install Protoc + uses: arduino/setup-protoc@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install gRPC Extension (for PHP 8.2 on Windows) + run: | + cd C:\tools\php + Invoke-WebRequest -Uri https://phpdev.toolsforresearch.com/php-8.2.7-nts-Win32-vs16-x64-grpc-protobuf.zip -OutFile php8.2.zip + unzip php8.2.zip ext/php_grpc.dll + rm php8.2.zip + echo "extension=php_grpc.dll" >> php.ini + if: ${{ matrix.operating-system == 'windows-latest' && matrix.php == '8.2' }} + shell: pwsh + - name: Composer install uses: ramsey/composer-install@v2 with: dependency-versions: ${{ matrix.dependencies }} + - name: Generate Library + run: composer gen-lib + - name: Composer test run: composer test diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 43a2591df..3bb3d8503 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -5,6 +5,7 @@ ->in(__DIR__ . '/tests') ->in(__DIR__ . '/example') ->in(__DIR__ . '/compatibility-suite/tests') + ->exclude('library/src') ->name('*.php'); $config = new PhpCsFixer\Config(); diff --git a/composer.json b/composer.json index 577c5b37f..af8416bd4 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ "guzzlehttp/guzzle": "^7.8", "behat/behat": "^3.13", "galbar/jsonpath": "^3.0", - "ramsey/uuid": "^4.7" + "ramsey/uuid": "^4.7", + "pact-foundation/example-protobuf-sync-message-provider": "@dev" }, "autoload": { "psr-4": { @@ -72,7 +73,14 @@ "MatchersProvider\\Tests\\": "example/matchers/provider/tests", "GeneratorsConsumer\\": "example/generators/consumer/src", "GeneratorsConsumer\\Tests\\": "example/generators/consumer/tests", - "GeneratorsProvider\\Tests\\": "example/generators/provider/tests" + "GeneratorsProvider\\Tests\\": "example/generators/provider/tests", + "": [ + "example/protobuf-sync-message/library/src" + ], + "ProtobufSyncMessageConsumer\\": "example/protobuf-sync-message/consumer/src", + "ProtobufSyncMessageConsumer\\Tests\\": "example/protobuf-sync-message/consumer/tests", + "ProtobufSyncMessageProvider\\": "example/protobuf-sync-message/provider/src", + "ProtobufSyncMessageProvider\\Tests\\": "example/protobuf-sync-message/provider/tests" } }, "scripts": { @@ -83,6 +91,9 @@ "php -r \"array_map('unlink', glob('./example/*/pacts/*.json'));\"", "phpunit --debug" ], + "gen-lib": [ + "protoc --php_out=example/protobuf-sync-message/library/src example/protobuf-sync-message/library/proto/area_calculator.proto" + ], "check-compatibility": "behat" }, "extra": { @@ -120,5 +131,11 @@ "allow-plugins": { "tienvx/composer-downloads-plugin": true } - } + }, + "repositories": [ + { + "type": "path", + "url": "example/protobuf-sync-message/provider" + } + ] } diff --git a/example/protobuf-sync-message/consumer/phpunit.xml b/example/protobuf-sync-message/consumer/phpunit.xml new file mode 100644 index 000000000..62a9eb007 --- /dev/null +++ b/example/protobuf-sync-message/consumer/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/protobuf-sync-message/consumer/src/CalculatorClient.php b/example/protobuf-sync-message/consumer/src/CalculatorClient.php new file mode 100644 index 000000000..4ce7497ac --- /dev/null +++ b/example/protobuf-sync-message/consumer/src/CalculatorClient.php @@ -0,0 +1,23 @@ +_simpleRequest( + '/plugins.Calculator/calculate', + $request, + [AreaResponse::class, 'decode'], + $metadata, + [] + )->wait(); + + return $response; + } +} diff --git a/example/protobuf-sync-message/consumer/src/ProtobufClient.php b/example/protobuf-sync-message/consumer/src/ProtobufClient.php new file mode 100644 index 000000000..44602d6ed --- /dev/null +++ b/example/protobuf-sync-message/consumer/src/ProtobufClient.php @@ -0,0 +1,23 @@ +baseUrl, [ + 'credentials' => ChannelCredentials::createInsecure(), + ]); + + return $client->calculate($shapeMessage); + } +} diff --git a/example/protobuf-sync-message/consumer/tests/ProtobufClientTest.php b/example/protobuf-sync-message/consumer/tests/ProtobufClientTest.php new file mode 100644 index 000000000..55cde3ccd --- /dev/null +++ b/example/protobuf-sync-message/consumer/tests/ProtobufClientTest.php @@ -0,0 +1,61 @@ +setConsumer('protobufSyncMessageConsumer'); + $config->setProvider('protobufSyncMessageProvider'); + $config->setPactSpecificationVersion('4.0.0'); + $config->setPactDir(__DIR__.'/../../pacts'); + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } + $config->setHost('127.0.0.1'); + $builder = new SyncMessageBuilder($config, new ProtobufSyncMessageDriverFactory()); + $builder + ->expectsToReceive('request for calculate shape area') + ->withMetadata([]) + ->withContent(new Text( + json_encode([ + 'pact:proto' => $protoPath, + 'pact:content-type' => 'application/grpc', + 'pact:proto-service' => 'Calculator/calculate', + + 'request' => [ + 'rectangle' => [ + 'length' => 'matching(number, 3)', + 'width' => 'matching(number, 4)', + ], + ], + 'response' => [ + 'value' => 'matching(number, 12)', + ] + ]), + 'application/grpc' + )); + $builder->registerMessage(); + + $service = new ProtobufClient("{$config->getHost()}:{$config->getPort()}"); + $rectangle = (new Rectangle())->setLength(3)->setWidth(4); + $message = (new ShapeMessage())->setRectangle($rectangle); + $response = $service->calculate($message); + + $this->assertTrue($builder->verify()); + $this->assertEquals(3 * 4, $response->getValue()); + } +} diff --git a/example/protobuf-sync-message/library/proto/area_calculator.proto b/example/protobuf-sync-message/library/proto/area_calculator.proto new file mode 100644 index 000000000..8bce6c994 --- /dev/null +++ b/example/protobuf-sync-message/library/proto/area_calculator.proto @@ -0,0 +1,47 @@ +syntax = "proto3"; + +package plugins; + +option php_generic_services = true; + +service Calculator { + rpc calculate (ShapeMessage) returns (AreaResponse) {} +} + +message ShapeMessage { + oneof shape { + Square square = 1; + Rectangle rectangle = 2; + Circle circle = 3; + Triangle triangle = 4; + Parallelogram parallelogram = 5; + } +} + +message Square { + float edge_length = 1; +} + +message Rectangle { + float length = 1; + float width = 2; +} + +message Circle { + float radius = 1; +} + +message Triangle { + float edge_a = 1; + float edge_b = 2; + float edge_c = 3; +} + +message Parallelogram { + float base_length = 1; + float height = 2; +} + +message AreaResponse { + float value = 1; +} diff --git a/example/protobuf-sync-message/library/src/.gitignore b/example/protobuf-sync-message/library/src/.gitignore new file mode 100644 index 000000000..cde8069e1 --- /dev/null +++ b/example/protobuf-sync-message/library/src/.gitignore @@ -0,0 +1 @@ +*.php diff --git a/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json b/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json new file mode 100644 index 000000000..ab308e252 --- /dev/null +++ b/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json @@ -0,0 +1,104 @@ +{ + "consumer": { + "name": "protobufSyncMessageConsumer" + }, + "interactions": [ + { + "description": "request for calculate shape area", + "interactionMarkup": { + "markup": "```protobuf\nmessage AreaResponse {\n float value = 1;\n}\n```\n", + "markupType": "COMMON_MARK" + }, + "pending": false, + "pluginConfiguration": { + "protobuf": { + "descriptorKey": "6b90c212dfe22dc3c119d1c3fe42b5e1", + "service": "Calculator/calculate" + } + }, + "request": { + "contents": { + "content": "EgoNAABAQBUAAIBA", + "contentType": "application/protobuf;message=ShapeMessage", + "contentTypeHint": "BINARY", + "encoded": "base64" + }, + "matchingRules": { + "body": { + "$.rectangle.length": { + "combine": "AND", + "matchers": [ + { + "match": "number" + } + ] + }, + "$.rectangle.width": { + "combine": "AND", + "matchers": [ + { + "match": "number" + } + ] + } + } + }, + "metadata": { + "contentType": "application/protobuf;message=ShapeMessage" + } + }, + "response": [ + { + "contents": { + "content": "DQAAQEE=", + "contentType": "application/protobuf;message=AreaResponse", + "contentTypeHint": "BINARY", + "encoded": "base64" + }, + "matchingRules": { + "body": { + "$.value": { + "combine": "AND", + "matchers": [ + { + "match": "number" + } + ] + } + } + }, + "metadata": { + "contentType": "application/protobuf;message=AreaResponse" + } + } + ], + "transport": "grpc", + "type": "Synchronous/Messages" + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.11", + "mockserver": "1.2.4", + "models": "1.1.12" + }, + "pactSpecification": { + "version": "4.0" + }, + "plugins": [ + { + "configuration": { + "6b90c212dfe22dc3c119d1c3fe42b5e1": { + "protoDescriptors": "CtYFChVhcmVhX2NhbGN1bGF0b3IucHJvdG8SB3BsdWdpbnMikgIKDFNoYXBlTWVzc2FnZRIpCgZzcXVhcmUYASABKAsyDy5wbHVnaW5zLlNxdWFyZUgAUgZzcXVhcmUSMgoJcmVjdGFuZ2xlGAIgASgLMhIucGx1Z2lucy5SZWN0YW5nbGVIAFIJcmVjdGFuZ2xlEikKBmNpcmNsZRgDIAEoCzIPLnBsdWdpbnMuQ2lyY2xlSABSBmNpcmNsZRIvCgh0cmlhbmdsZRgEIAEoCzIRLnBsdWdpbnMuVHJpYW5nbGVIAFIIdHJpYW5nbGUSPgoNcGFyYWxsZWxvZ3JhbRgFIAEoCzIWLnBsdWdpbnMuUGFyYWxsZWxvZ3JhbUgAUg1wYXJhbGxlbG9ncmFtQgcKBXNoYXBlIikKBlNxdWFyZRIfCgtlZGdlX2xlbmd0aBgBIAEoAlIKZWRnZUxlbmd0aCI5CglSZWN0YW5nbGUSFgoGbGVuZ3RoGAEgASgCUgZsZW5ndGgSFAoFd2lkdGgYAiABKAJSBXdpZHRoIiAKBkNpcmNsZRIWCgZyYWRpdXMYASABKAJSBnJhZGl1cyJPCghUcmlhbmdsZRIVCgZlZGdlX2EYASABKAJSBWVkZ2VBEhUKBmVkZ2VfYhgCIAEoAlIFZWRnZUISFQoGZWRnZV9jGAMgASgCUgVlZGdlQyJICg1QYXJhbGxlbG9ncmFtEh8KC2Jhc2VfbGVuZ3RoGAEgASgCUgpiYXNlTGVuZ3RoEhYKBmhlaWdodBgCIAEoAlIGaGVpZ2h0IiQKDEFyZWFSZXNwb25zZRIUCgV2YWx1ZRgBIAEoAlIFdmFsdWUySQoKQ2FsY3VsYXRvchI7CgljYWxjdWxhdGUSFS5wbHVnaW5zLlNoYXBlTWVzc2FnZRoVLnBsdWdpbnMuQXJlYVJlc3BvbnNlIgBCA9ACAWIGcHJvdG8z", + "protoFile": "syntax = \"proto3\";\n\npackage plugins;\n\noption php_generic_services = true;\n\nservice Calculator {\n rpc calculate (ShapeMessage) returns (AreaResponse) {}\n}\n\nmessage ShapeMessage {\n oneof shape {\n Square square = 1;\n Rectangle rectangle = 2;\n Circle circle = 3;\n Triangle triangle = 4;\n Parallelogram parallelogram = 5;\n }\n}\n\nmessage Square {\n float edge_length = 1;\n}\n\nmessage Rectangle {\n float length = 1;\n float width = 2;\n}\n\nmessage Circle {\n float radius = 1;\n}\n\nmessage Triangle {\n float edge_a = 1;\n float edge_b = 2;\n float edge_c = 3;\n}\n\nmessage Parallelogram {\n float base_length = 1;\n float height = 2;\n}\n\nmessage AreaResponse {\n float value = 1;\n}\n" + } + }, + "name": "protobuf", + "version": "0.3.8" + } + ] + }, + "provider": { + "name": "protobufSyncMessageProvider" + } +} \ No newline at end of file diff --git a/example/protobuf-sync-message/provider/.rr.yaml b/example/protobuf-sync-message/provider/.rr.yaml new file mode 100644 index 000000000..25f94af25 --- /dev/null +++ b/example/protobuf-sync-message/provider/.rr.yaml @@ -0,0 +1,11 @@ +version: "2.7" + +server: + command: "php worker.php" + +grpc: + listen: "tcp://127.0.0.1:9001" + proto: + - "../library/proto/area_calculator.proto" + pool: + num_workers: 1 diff --git a/example/protobuf-sync-message/provider/composer.json b/example/protobuf-sync-message/provider/composer.json new file mode 100644 index 000000000..2eccb6faa --- /dev/null +++ b/example/protobuf-sync-message/provider/composer.json @@ -0,0 +1,30 @@ +{ + "name": "pact-foundation/example-protobuf-sync-message-provider", + "require": { + "grpc/grpc": "^1.57", + "spiral/roadrunner-grpc": "^2.0", + "tienvx/composer-downloads-plugin": "^1.2" + }, + "require-dev": { + "ext-grpc": "*" + }, + "extra": { + "downloads": { + "rr": { + "version": "2023.3.8", + "variables": { + "{$os}": "strtolower(PHP_OS_FAMILY)", + "{$architecture}": "php_uname('m') === 'x86_64' ? 'amd64' : strtolower(php_uname('m'))", + "{$extension}": "(PHP_OS_FAMILY === 'Windows' || (PHP_OS_FAMILY === 'Darwin' && php_uname('m') === 'x86_64')) ? 'zip' : 'tar.gz'" + }, + "url": "https://github.com/roadrunner-server/roadrunner/releases/download/v{$version}/roadrunner-{$version}-{$os}-{$architecture}.{$extension}", + "path": "bin/roadrunner" + } + } + }, + "config": { + "allow-plugins": { + "tienvx/composer-downloads-plugin": true + } + } +} diff --git a/example/protobuf-sync-message/provider/phpunit.xml b/example/protobuf-sync-message/provider/phpunit.xml new file mode 100644 index 000000000..62a9eb007 --- /dev/null +++ b/example/protobuf-sync-message/provider/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/protobuf-sync-message/provider/src/Service/Calculator.php b/example/protobuf-sync-message/provider/src/Service/Calculator.php new file mode 100644 index 000000000..6ba7f07ab --- /dev/null +++ b/example/protobuf-sync-message/provider/src/Service/Calculator.php @@ -0,0 +1,71 @@ +getShape()) { + case 'square': + $area = $this->calculateSquareArea($request->getSquare()); + break; + case 'rectangle': + $area = $this->calculateRectangleArea($request->getRectangle()); + break; + case 'circle': + $area = $this->calculateCircleArea($request->getCircle()); + break; + case 'triangle': + $area = $this->calculateTriangleArea($request->getTriangle()); + break; + case 'parallelogram': + $area = $this->calculateParallelogramArea($request->getParallelogram()); + break; + default: + throw new Exception(sprintf('Shape %s is not supported', $request->getShape())); + } + + return new AreaResponse(['value' => $area]); + } + + private function calculateSquareArea(Square $square): float + { + return pow($square->getEdgeLength(), 2); + } + + private function calculateRectangleArea(Rectangle $rectangle): float + { + return $rectangle->getWidth() * $rectangle->getLength(); + } + + private function calculateCircleArea(Circle $circle): float + { + return pi() * pow($circle->getRadius(), 2); + } + + /** + * Use Heron's formula. + */ + private function calculateTriangleArea(Triangle $triangle): float + { + $p = ($triangle->getEdgeA() + $triangle->getEdgeB() + $triangle->getEdgeC()) / 2; + + return sqrt($p * ($p - $triangle->getEdgeA()) * ($p - $triangle->getEdgeB()) * ($p - $triangle->getEdgeC())); + } + + private function calculateParallelogramArea(Parallelogram $parallelogram): float + { + return $parallelogram->getBaseLength() * $parallelogram->getHeight(); + } +} diff --git a/example/protobuf-sync-message/provider/src/Service/CalculatorInterface.php b/example/protobuf-sync-message/provider/src/Service/CalculatorInterface.php new file mode 100644 index 000000000..990b455ad --- /dev/null +++ b/example/protobuf-sync-message/provider/src/Service/CalculatorInterface.php @@ -0,0 +1,15 @@ +process = new Process([__DIR__ . '/../bin/roadrunner/rr', 'serve', '-w', __DIR__ . '/..']); + $this->process->setTimeout(120); + + $this->process->start(function (string $type, string $buffer): void { + echo "\n$type > $buffer"; + }); + $this->process->waitUntil(fn () => is_resource(@fsockopen('127.0.0.1', 9001))); + } + + protected function tearDown(): void + { + $this->process->stop(); + } + + public function testPactVerifyConsumer(): void + { + $config = new VerifierConfig(); + $config->getProviderInfo() + ->setName('protobufSyncMessageProvider') + ->setHost('127.0.0.1'); + $providerTransport = new ProviderTransport(); + $providerTransport + ->setProtocol('grpc') + ->setScheme('tcp') + ->setPort(9001) + ->setPath('/') + ; + $config->addProviderTransport($providerTransport); + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } + + $verifier = new Verifier($config); + $verifier->addFile(__DIR__ . '/../../pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json'); + + $this->assertTrue($verifier->verify()); + } +} diff --git a/example/protobuf-sync-message/provider/worker.php b/example/protobuf-sync-message/provider/worker.php new file mode 100644 index 000000000..55c59fb38 --- /dev/null +++ b/example/protobuf-sync-message/provider/worker.php @@ -0,0 +1,17 @@ + false, // optional (default: false) +]); + +$server->registerService(CalculatorInterface::class, new Calculator()); + +$server->serve(Worker::create()); diff --git a/phpunit.xml b/phpunit.xml index e300d5a8c..b4e7bf885 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -55,6 +55,12 @@ ./example/generators/provider/tests + + ./example/protobuf-sync-message/consumer/tests + + + ./example/protobuf-sync-message/provider/tests +