diff --git a/composer.json b/composer.json index f41bb1f1f1d..8cc315d3981 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "php-mock/php-mock-phpunit": "^2.10", "phpstan/phpstan": "^1.12", "nyholm/psr7": "^1.8", + "guzzlehttp/guzzle": "^7.8", "ext-json": "*", "ext-xml": "*", "ext-curl": "*", diff --git a/test/known_failures_Linux.json b/test/known_failures_Linux.json index c85c25bc185..e283fd97116 100644 --- a/test/known_failures_Linux.json +++ b/test/known_failures_Linux.json @@ -588,6 +588,10 @@ "nodejs-nodejs_compact_websocket-domain", "nodejs-nodejs_header_websocket-domain", "nodejs-nodejs_json_websocket-domain", + "nodejs-php_binary-accel_http-ip", + "nodejs-php_binary_http-ip", + "nodejs-php_compact_http-ip", + "nodejs-php_json_http-ip", "nodejs-py_binary-accel_http-domain", "nodejs-py_binary-accel_http-ip", "nodejs-py_binary-accel_http-ip-ssl", @@ -615,6 +619,10 @@ "perl-netstd_multi-binary_buffered-ip-ssl", "perl-netstd_multi-binary_framed-ip", "perl-netstd_multi-binary_framed-ip-ssl", + "php-cpp_accel-binary_http-ip", + "php-cpp_binary_http-ip", + "php-cpp_compact_http-ip", + "php-cpp_json_http-ip", "py-cpp_accel-binary_http-domain", "py-cpp_accel-binary_http-ip", "py-cpp_accel-binary_http-ip-ssl", diff --git a/test/php/HttpRouter.php b/test/php/HttpRouter.php new file mode 100644 index 00000000000..16d1ccd8f7f --- /dev/null +++ b/test/php/HttpRouter.php @@ -0,0 +1,77 @@ +registerDefinition('ThriftTest', __DIR__ . '/gen-php-classmap'); + $loader->register(); + + require_once __DIR__ . '/Handler.php'; + + $protocolFactory = thrift_test_protocol_factory(getenv('THRIFT_TEST_PROTOCOL') ?: 'binary'); + $processor = new ThriftTest\ThriftTestProcessor(new Handler()); + + (new HttpServer($processor, $protocolFactory))->serve(); + return; +} + +// CLI launcher: sourced from TestServer.php; $port and $protocol are in scope. +if (!function_exists('pcntl_exec')) { + fwrite(STDERR, "PHP HTTP cross-test requires ext-pcntl.\n"); + exit(1); +} + +$env = getenv(); +$env['THRIFT_TEST_PROTOCOL'] = $protocol; + +echo "Starting the Test server (HTTP via php -S)...\n"; + +// -d flags mirror the launcher invocation in test/tests.json — keep in sync. +pcntl_exec(PHP_BINARY, [ + '-dextension_dir=php_ext_dir', + '-dextension=thrift_protocol.so', + '-ddisplay_errors=stderr', + '-dlog_errors=0', + '-derror_reporting=E_ALL', + '-S', '127.0.0.1:' . $port, + __FILE__, +], $env); + +fwrite(STDERR, "pcntl_exec failed\n"); +exit(1); diff --git a/test/php/HttpServer.php b/test/php/HttpServer.php new file mode 100644 index 00000000000..0e074ba62da --- /dev/null +++ b/test/php/HttpServer.php @@ -0,0 +1,117 @@ +peekMessageType($requestBody) === TMessageType::ONEWAY + && function_exists('pcntl_fork') + && $this->dispatchOnewayAsync($requestBody) + ) { + return; + } + + $this->dispatchSync($requestBody); + } + + private function peekMessageType(string $body): ?int + { + if ($body === '') { + return null; + } + try { + $type = null; + $name = null; + $seqid = null; + $this->protocolFactory + ->getProtocol(new TMemoryBuffer($body)) + ->readMessageBegin($name, $type, $seqid); + + return $type; + } catch (\Throwable) { + return null; + } + } + + private function dispatchOnewayAsync(string $body): bool + { + header('Content-Length: 0'); + $pid = pcntl_fork(); + if ($pid > 0) { + return true; + } + if ($pid === 0) { + if (defined('STDIN')) { + fclose(STDIN); + } + if (defined('STDOUT')) { + fclose(STDOUT); + } + if (defined('STDERR')) { + fclose(STDERR); + } + $this->processor->process( + $this->protocolFactory->getProtocol(new TMemoryBuffer($body)), + $this->protocolFactory->getProtocol(new TMemoryBuffer()), + ); + exit(0); + } + + return false; + } + + private function dispatchSync(string $body): void + { + $output = new TMemoryBuffer(); + $this->processor->process( + $this->protocolFactory->getProtocol(new TMemoryBuffer($body)), + $this->protocolFactory->getProtocol($output), + ); + echo $output->getBuffer(); + } +} diff --git a/test/php/TestClient.php b/test/php/TestClient.php index c9e1ba7e5ec..20e5b1a5e4b 100755 --- a/test/php/TestClient.php +++ b/test/php/TestClient.php @@ -1,28 +1,5 @@ addPsr4('', $GEN_DIR); -} else { - $loader = new ThriftClassLoader(); - $loader->registerDefinition('ThriftTest', $GEN_DIR); - $loader->register(); -} - /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -42,23 +19,35 @@ * under the License. */ +namespace test\php; + use Psr\Log\AbstractLogger; use Psr\Log\LoggerInterface; +use Thrift\ClassLoader\ThriftClassLoader; +use Thrift\Transport\TBufferedTransport; +use Thrift\Transport\TFramedTransport; +use Thrift\Transport\TPsrHttpClient; +use Thrift\Transport\TSocketPool; -/** Include the Thrift base */ -/** Include the protocols */ -use Thrift\Protocol\TBinaryProtocol; -use Thrift\Protocol\TBinaryProtocolAccelerated; -use Thrift\Protocol\TCompactProtocol; -use Thrift\Protocol\TJSONProtocol; +/** @var \Composer\Autoload\ClassLoader $loader */ +$loader = require __DIR__ . '/../../vendor/autoload.php'; -/** Include the socket layer */ -use Thrift\Transport\TSocket; -use Thrift\Transport\TSocketPool; +if (!isset($GEN_DIR)) { + $GEN_DIR = 'gen-php'; +} +if (!isset($MODE)) { + $MODE = 'normal'; +} -/** Include the socket layer */ -use Thrift\Transport\TFramedTransport; -use Thrift\Transport\TBufferedTransport; +if ($GEN_DIR == 'gen-php') { + $loader->addPsr4('', $GEN_DIR); +} else { + $loader = new ThriftClassLoader(); + $loader->registerDefinition('ThriftTest', $GEN_DIR); + $loader->register(); +} + +require_once __DIR__ . '/protocols.php'; /** * Minimal PSR-3 logger that forwards every message to PHP's error_log @@ -78,67 +67,33 @@ public function log($level, $message, array $context = []): void } } -function makeProtocol($transport, $PROTO) -{ - if ($PROTO == 'binary') { - return new TBinaryProtocol($transport); - } else if ($PROTO == 'compact') { - return new TCompactProtocol($transport); - } else if ($PROTO == 'json') { - return new TJSONProtocol($transport); - } else if ($PROTO == 'accel') { - if (!function_exists('thrift_protocol_write_binary')) { - echo "Acceleration extension is not loaded\n"; - exit(1); - } - return new TBinaryProtocolAccelerated($transport); - } - - echo "--protocol must be one of {binary|compact|json|accel}\n"; - exit(1); -} - -$host = 'localhost'; $port = 9090; -if ($argc > 1) { - $host = $argv[0]; +foreach ($argv as $arg) { + if (substr($arg, 0, 7) == '--port=') { + $port = (int) substr($arg, 7); + } elseif (substr($arg, 0, 12) == '--transport=') { + $MODE = substr($arg, 12); + } elseif (substr($arg, 0, 11) == '--protocol=') { + $PROTO = substr($arg, 11); + } } -if ($argc > 2) { - $host = $argv[1]; -} +// TPsrHttpClient buffers internally, so no framed/buffered wrapper is needed. +// Inline mode passes the raw transport to the generated client without a +// protocol wrapper, matching the legacy code path. +$transport = match ($MODE) { + 'http' => new TPsrHttpClient(sprintf('http://127.0.0.1:%d/', $port)), + default => new TSocketPool(['localhost'], $port, false, new StderrLogger()), +}; +$transport = match ($MODE) { + 'framed' => new TFramedTransport($transport), + 'http', 'inline' => $transport, + default => new TBufferedTransport($transport, 1024, 1024), +}; -foreach ($argv as $arg) { - if (substr($arg, 0, 7) == '--port=') { - $port = (int) substr($arg, 7); - } else if (substr($arg, 0, 12) == '--transport=') { - $MODE = substr($arg, 12); - } else if (substr($arg, 0, 11) == '--protocol=') { - $PROTO = substr($arg, 11); - } -} - -$hosts = array('localhost'); - -$logger = new StderrLogger(); -$socket = new TSocket($host, $port, false, $logger); -$socket = new TSocketPool($hosts, $port, false, $logger); - -if ($MODE == 'inline') { - $transport = $socket; - $testClient = new \ThriftTest\ThriftTestClient($transport); -} else if ($MODE == 'framed') { - $framedSocket = new TFramedTransport($socket); - $transport = $framedSocket; - $protocol = makeProtocol($transport, $PROTO); - $testClient = new \ThriftTest\ThriftTestClient($protocol); -} else { - $bufferedSocket = new TBufferedTransport($socket, 1024, 1024); - $transport = $bufferedSocket; - $protocol = makeProtocol($transport, $PROTO); - $testClient = new \ThriftTest\ThriftTestClient($protocol); -} +$protocol = $MODE === 'inline' ? null : thrift_test_protocol_factory($PROTO)->getProtocol($transport); +$testClient = new \ThriftTest\ThriftTestClient($protocol ?? $transport); $transport->open(); @@ -150,6 +105,7 @@ function makeProtocol($transport, $PROTO) define('ERR_EXCEPTIONS', 8); define('ERR_UNKNOWN', 64); $exitcode = 0; + /** * VOID TEST */ @@ -157,15 +113,16 @@ function makeProtocol($transport, $PROTO) $testClient->testVoid(); print_r(" = void\n"); -function roundtrip($testClient, $method, $value) { - global $exitcode; - print_r("$method($value)"); - $ret = $testClient->$method($value); - print_r(" = \"$ret\"\n"); - if ($value !== $ret) { - print_r("*** FAILED ***\n"); - $exitcode |= ERR_BASETYPES; - } +function roundtrip($testClient, $method, $value) +{ + global $exitcode; + print_r("$method($value)"); + $ret = $testClient->$method($value); + print_r(" = \"$ret\"\n"); + if ($value !== $ret) { + print_r("*** FAILED ***\n"); + $exitcode |= ERR_BASETYPES; + } } /** @@ -234,10 +191,10 @@ function roundtrip($testClient, $method, $value) { $out->i32_thing = -3; $out->i64_thing = -5; $in = $testClient->testStruct($out); -print_r(" = {\"".$in->string_thing."\", ". - $in->byte_thing.", ". - $in->i32_thing.", ". - $in->i64_thing."}\n"); +print_r(" = {\"" . $in->string_thing . "\", " . + $in->byte_thing . ", " . + $in->i32_thing . ", " . + $in->i64_thing . "}\n"); if ($in != $out) { echo "**FAILED**\n"; @@ -254,12 +211,12 @@ function roundtrip($testClient, $method, $value) { $out2->i32_thing = 5; $in2 = $testClient->testNest($out2); $in = $in2->struct_thing; -print_r(" = {".$in2->byte_thing.", {\"". - $in->string_thing."\", ". - $in->byte_thing.", ". - $in->i32_thing.", ". - $in->i64_thing."}, ". - $in2->i32_thing."}\n"); +print_r(" = {" . $in2->byte_thing . ", {\"" . + $in->string_thing . "\", " . + $in->byte_thing . ", " . + $in->i32_thing . ", " . + $in->i64_thing . "}, " . + $in2->i32_thing . "}\n"); if ($in2 != $out2) { echo "**FAILED**\n"; @@ -269,19 +226,19 @@ function roundtrip($testClient, $method, $value) { /** * MAP TEST */ -$mapout = array(); +$mapout = []; for ($i = 0; $i < 5; ++$i) { - $mapout[$i] = $i-10; + $mapout[$i] = $i - 10; } print_r("testMap({"); $first = true; foreach ($mapout as $key => $val) { - if ($first) { - $first = false; - } else { - print_r(", "); - } - print_r("$key => $val"); + if ($first) { + $first = false; + } else { + print_r(", "); + } + print_r("$key => $val"); } print_r("})"); @@ -289,12 +246,12 @@ function roundtrip($testClient, $method, $value) { print_r(" = {"); $first = true; foreach ($mapin as $key => $val) { - if ($first) { - $first = false; - } else { - print_r(", "); - } - print_r("$key => $val"); + if ($first) { + $first = false; + } else { + print_r(", "); + } + print_r("$key => $val"); } print_r("}\n"); @@ -303,31 +260,31 @@ function roundtrip($testClient, $method, $value) { $exitcode |= ERR_CONTAINERS; } -$mapout = array(); +$mapout = []; for ($i = 0; $i < 10; $i++) { $mapout["key$i"] = "val$i"; } print_r('testStringMap({'); $first = true; foreach ($mapout as $key => $val) { - if ($first) { - $first = false; - } else { - print_r(", "); - } - print_r("\"$key\" => \"$val\""); + if ($first) { + $first = false; + } else { + print_r(", "); + } + print_r("\"$key\" => \"$val\""); } print_r("})"); $mapin = $testClient->testStringMap($mapout); print_r(" = {"); $first = true; foreach ($mapin as $key => $val) { - if ($first) { - $first = false; - } else { - print_r(", "); - } - print_r("\"$key\" => \"$val\""); + if ($first) { + $first = false; + } else { + print_r(", "); + } + print_r("\"$key\" => \"$val\""); } print_r("}\n"); ksort($mapin); @@ -339,9 +296,9 @@ function roundtrip($testClient, $method, $value) { /** * SET TEST */ -$setout = array();; +$setout = []; for ($i = -2; $i < 3; ++$i) { - $setout[$i]= true; + $setout[$i] = true; } print_r("testSet({"); echo implode(',', array_keys($setout)); @@ -365,31 +322,31 @@ function roundtrip($testClient, $method, $value) { /** * LIST TEST */ -$listout = array(); +$listout = []; for ($i = -2; $i < 3; ++$i) { - $listout []= $i; + $listout[] = $i; } print_r("testList({"); $first = true; foreach ($listout as $val) { - if ($first) { - $first = false; - } else { - print_r(", "); - } - print_r($val); + if ($first) { + $first = false; + } else { + print_r(", "); + } + print_r($val); } print_r("})"); $listin = $testClient->testList($listout); print_r(" = {"); $first = true; foreach ($listin as $val) { - if ($first) { - $first = false; - } else { - print_r(", "); - } - print_r($val); + if ($first) { + $first = false; + } else { + print_r(", "); + } + print_r($val); } print_r("}\n"); if ($listin !== $listout) { @@ -458,16 +415,16 @@ function roundtrip($testClient, $method, $value) { $mm = $testClient->testMapMap(1); print_r(" = {"); foreach ($mm as $key => $val) { - print_r("$key => {"); - foreach ($val as $k2 => $v2) { - print_r("$k2 => $v2, "); - } - print_r("}, "); + print_r("$key => {"); + foreach ($val as $k2 => $v2) { + print_r("$k2 => $v2, "); + } + print_r("}, "); } print_r("}\n"); $expected_mm = [ - -4 => [-4 => -4, -3 => -3, -2 => -2, -1 => -1], - 4 => [4 => 4, 3 => 3, 2 => 2, 1 => 1], + -4 => [-4 => -4, -3 => -3, -2 => -2, -1 => -1], + 4 => [4 => 4, 3 => 3, 2 => 2, 1 => 1], ]; if ($mm != $expected_mm) { echo "**FAILED**\n"; @@ -484,36 +441,36 @@ function roundtrip($testClient, $method, $value) { $truck->byte_thing = 8; $truck->i32_thing = 8; $truck->i64_thing = 8; -$insane->xtructs []= $truck; +$insane->xtructs[] = $truck; print_r("testInsanity()"); $whoa = $testClient->testInsanity($insane); print_r(" = {"); foreach ($whoa as $key => $val) { - print_r("$key => {"); - foreach ($val as $k2 => $v2) { - print_r("$k2 => {"); - $userMap = $v2->userMap; - print_r("{"); - if (is_array($userMap)) { - foreach ($userMap as $k3 => $v3) { - print_r("$k3 => $v3, "); - } + print_r("$key => {"); + foreach ($val as $k2 => $v2) { + print_r("$k2 => {"); + $userMap = $v2->userMap; + print_r("{"); + if (is_array($userMap)) { + foreach ($userMap as $k3 => $v3) { + print_r("$k3 => $v3, "); + } + } + print_r("}, "); + + $xtructs = $v2->xtructs; + print_r("{"); + if (is_array($xtructs)) { + foreach ($xtructs as $x) { + print_r("{\"" . $x->string_thing . "\", " . + $x->byte_thing . ", " . $x->i32_thing . ", " . $x->i64_thing . "}, "); + } + } + print_r("}"); + + print_r("}, "); } print_r("}, "); - - $xtructs = $v2->xtructs; - print_r("{"); - if (is_array($xtructs)) { - foreach ($xtructs as $x) { - print_r("{\"".$x->string_thing."\", ". - $x->byte_thing.", ".$x->i32_thing.", ".$x->i64_thing."}, "); - } - } - print_r("}"); - - print_r("}, "); - } - print_r("}, "); } print_r("}\n"); @@ -522,30 +479,30 @@ function roundtrip($testClient, $method, $value) { */ print_r("testException('Xception')"); try { - $testClient->testException('Xception'); - print_r(" void\nFAILURE\n"); - $exitcode |= ERR_EXCEPTIONS; + $testClient->testException('Xception'); + print_r(" void\nFAILURE\n"); + $exitcode |= ERR_EXCEPTIONS; } catch (\ThriftTest\Xception $x) { - print_r(' caught xception '.$x->errorCode.': '.$x->message."\n"); + print_r(' caught xception ' . $x->errorCode . ': ' . $x->message . "\n"); } // Regression test for THRIFT-4263 print_r("testBinarySerializer_Deserialize('foo')"); try { - \Thrift\Serializer\TBinarySerializer::deserialize(base64_decode('foo'), \ThriftTest\Xtruct2::class); - echo "**FAILED**\n"; - $exitcode |= ERR_STRUCTS; + \Thrift\Serializer\TBinarySerializer::deserialize(base64_decode('foo'), \ThriftTest\Xtruct2::class); + echo "**FAILED**\n"; + $exitcode |= ERR_STRUCTS; } catch (\Thrift\Exception\TTransportException $happy_exception) { - // We expected this due to binary data of base64_decode('foo') is less then 4 - // bytes and it tries to find thrift version number in the transport by - // reading i32() at the beginning. Casting to string validates that - // exception is still accessible in memory and not corrupted. Without patch, - // PHP will error log that the exception doesn't have any tostring method, - // which is a lie due to corrupted memory. - for($i=99; $i > 0; $i--) { - (string)$happy_exception; - } - print_r(" SUCCESS\n"); + // We expected this due to binary data of base64_decode('foo') is less then 4 + // bytes and it tries to find thrift version number in the transport by + // reading i32() at the beginning. Casting to string validates that + // exception is still accessible in memory and not corrupted. Without patch, + // PHP will error log that the exception doesn't have any tostring method, + // which is a lie due to corrupted memory. + for ($i = 99; $i > 0; $i--) { + (string) $happy_exception; + } + print_r(" SUCCESS\n"); } /** @@ -553,14 +510,14 @@ function roundtrip($testClient, $method, $value) { */ $stop = microtime(true); -$elp = round(1000*($stop - $start), 0); +$elp = round(1000 * ($stop - $start), 0); print_r("Total time: $elp ms\n"); /** * Extraneous "I don't trust PHP to pack/unpack integer" tests */ -if ($protocol instanceof TBinaryProtocolAccelerated) { +if ($protocol instanceof \Thrift\Protocol\TBinaryProtocolAccelerated) { // Regression check: check that method name is not double-freed // Method name should not be an interned string. $method_name = "Void"; @@ -570,7 +527,6 @@ function roundtrip($testClient, $method, $value) { $args = new \ThriftTest\ThriftTest_testVoid_args(); thrift_protocol_write_binary($protocol, $method_name, \Thrift\Type\TMessageType::CALL, $args, $seqid, $protocol->isStrictWrite()); $testClient->recv_testVoid(); - } // Max I32 diff --git a/test/php/TestServer.php b/test/php/TestServer.php index 4ab773cb88c..bdcb25056c9 100644 --- a/test/php/TestServer.php +++ b/test/php/TestServer.php @@ -3,6 +3,7 @@ error_reporting(E_ALL); require_once __DIR__ . '/../../vendor/autoload.php'; +require_once __DIR__ . '/protocols.php'; $opts = getopt( 'h::', @@ -47,57 +48,30 @@ $transport = $opts['transport'] ?? 'buffered'; $protocol = $opts['protocol'] ?? 'binary'; +// HTTP transport: delegate to HttpRouter (its CLI-launcher branch execs into +// `php -S` with HttpRouter itself as the per-request handler). +if ($transport === 'http') { + require __DIR__ . '/HttpRouter.php'; + return; +} -$loader = new Thrift\ClassLoader\ThriftClassLoader(); +$loader = new \Thrift\ClassLoader\ThriftClassLoader(); $loader->registerDefinition('ThriftTest', __DIR__ . '/gen-php-classmap'); $loader->register(); -$sslOptions = \stream_context_create( - [ - 'ssl' => [ - 'verify_peer' => false, - 'verify_peer_name' => false, - ], - ] -); - require_once __DIR__ . '/Handler.php'; -switch ($transport) { - case 'framed': - $serverTransportFactory = new \Thrift\Factory\TFramedTransportFactory(); - break; - default: - $serverTransportFactory = new \Thrift\Factory\TTransportFactory(); -} +$serverTransportFactory = match ($transport) { + 'framed' => new \Thrift\Factory\TFramedTransportFactory(), + default => new \Thrift\Factory\TTransportFactory(), +}; -switch ($protocol) { - case 'binary': - $protocolFactory = new \Thrift\Factory\TBinaryProtocolFactory(false, true); - break; - case 'accel': - if (!function_exists('thrift_protocol_write_binary')) { - fwrite(STDERR, "Acceleration extension is not loaded\n"); - exit(1); - } - $protocolFactory = new \Thrift\Factory\TBinaryProtocolAcceleratedFactory(); - break; - case 'compact': - $protocolFactory = new \Thrift\Factory\TCompactProtocolFactory(); - break; - case 'json': - $protocolFactory = new \Thrift\Factory\TJSONProtocolFactory(); - break; - default: - fwrite(STDERR, "--protocol must be one of {binary|compact|json|accel}\n"); - exit(1); -} +$protocolFactory = thrift_test_protocol_factory($protocol); // `localhost` may resolve to an IPv6-only listener in newer PHP/runtime combinations, // while some cross-test clients still connect via 127.0.0.1. Bind explicitly to IPv4. $serverTransport = new \Thrift\Server\TServerSocket('127.0.0.1', $port); -$handler = new Handler(); -$processor = new ThriftTest\ThriftTestProcessor($handler); +$processor = new \ThriftTest\ThriftTestProcessor(new \Handler()); $server = new \Thrift\Server\TSimpleServer( $processor, @@ -105,7 +79,7 @@ $serverTransportFactory, $serverTransportFactory, $protocolFactory, - $protocolFactory + $protocolFactory, ); echo "Starting the Test server...\n"; diff --git a/test/php/protocols.php b/test/php/protocols.php new file mode 100644 index 00000000000..64f2287202b --- /dev/null +++ b/test/php/protocols.php @@ -0,0 +1,49 @@ +