diff --git a/lib/php/lib/Protocol/TCompactProtocol.php b/lib/php/lib/Protocol/TCompactProtocol.php index ffa6560be48..5839dbfbcf0 100644 --- a/lib/php/lib/Protocol/TCompactProtocol.php +++ b/lib/php/lib/Protocol/TCompactProtocol.php @@ -702,8 +702,16 @@ public function readI64(&$value) public function writeI64($value) { - // If we are in an I32 range, use the easy method below. - if (($value > 4294967296) || ($value < -4294967296)) { + if ($value === PHP_INT_MIN) { + // PHP_INT_MIN (-2^63) cannot be safely negated: -PHP_INT_MIN overflows + // the 64-bit signed integer range. Its zigzag encoding is the maximum + // unsigned 64-bit varint (0xFFFFFFFFFFFFFFFF), so we write it directly. + + $out = "\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01"; + $this->trans_->write($out, 10); + + return 10; + } elseif (($value > 4294967296) || ($value < -4294967296)) { // Convert $value to $hi and $lo $neg = $value < 0; diff --git a/lib/php/test/Unit/Lib/Exception/TExceptionTest.php b/lib/php/test/Unit/Lib/Exception/TExceptionTest.php index add8803b0ea..f517cfef413 100644 --- a/lib/php/test/Unit/Lib/Exception/TExceptionTest.php +++ b/lib/php/test/Unit/Lib/Exception/TExceptionTest.php @@ -22,7 +22,11 @@ namespace Test\Thrift\Unit\Lib\Exception; use PHPUnit\Framework\TestCase; +use Test\Thrift\Unit\Lib\Fixture\TestRichException; use Thrift\Exception\TException; +use Thrift\Protocol\TBinaryProtocol; +use Thrift\Transport\TMemoryBuffer; +use Thrift\Type\TType; class TExceptionTest extends TestCase { @@ -57,4 +61,163 @@ public function testExceptionWithSpecAndVals() $this->assertEquals(123456, $exception->int); $this->assertEquals(true, $exception->bool); } + + public function testExceptionWithDefaultParams() + { + $exception = new TException(); + $this->assertSame('', $exception->getMessage()); + $this->assertSame(0, $exception->getCode()); + } + + public function testExceptionSpecIgnoresUnsetVals() + { + $spec = [ + ['var' => 'field1'], + ['var' => 'field2'], + ]; + $vals = ['field1' => 'set']; + + $exception = new TException($spec, $vals); + $this->assertEquals('set', $exception->field1); + } + + /** + * @dataProvider writeAndReadFieldDataProvider + */ + public function testWriteAndReadField($field, $value) + { + $exception = new TestRichException(); + $exception->$field = $value; + + $result = $this->roundtrip($exception); + + $this->assertEquals($value, $result->$field); + } + + public function writeAndReadFieldDataProvider() + { + // scalars + yield 'string' => ['field' => 'stringField', 'value' => 'hello world']; + yield 'int' => ['field' => 'intField', 'value' => 42]; + yield 'bool true' => ['field' => 'boolField', 'value' => true]; + yield 'bool false' => ['field' => 'boolField', 'value' => false]; + yield 'double' => ['field' => 'doubleField', 'value' => 3.14]; + + // containers + yield 'map' => ['field' => 'mapField', 'value' => ['key1' => 100, 'key2' => 200]]; + yield 'list' => ['field' => 'listField', 'value' => ['alpha', 'beta', 'gamma']]; + yield 'set' => ['field' => 'setField', 'value' => [10 => true, 20 => true, 30 => true]]; + + // empty containers + yield 'empty map' => ['field' => 'mapField', 'value' => []]; + yield 'empty list' => ['field' => 'listField', 'value' => []]; + yield 'empty set' => ['field' => 'setField', 'value' => []]; + } + + public function testWriteAndReadAllFields() + { + $exception = new TestRichException(); + $exception->stringField = 'test'; + $exception->intField = 99; + $exception->boolField = true; + $exception->doubleField = 2.718; + $exception->mapField = ['a' => 1]; + $exception->listField = ['x', 'y']; + $exception->setField = [5 => true]; + + $result = $this->roundtrip($exception); + + $this->assertEquals('test', $result->stringField); + $this->assertEquals(99, $result->intField); + $this->assertTrue($result->boolField); + $this->assertEquals(2.718, $result->doubleField); + $this->assertEquals(['a' => 1], $result->mapField); + $this->assertEquals(['x', 'y'], $result->listField); + $this->assertEquals([5 => true], $result->setField); + } + + public function testWriteSkipsNullFields() + { + $exception = new TestRichException(); + $exception->stringField = 'only this'; + + $transport = new TMemoryBuffer(); + $protocol = new TBinaryProtocol($transport); + $exception->write($protocol); + + $result = new TestRichException(); + $readTransport = new TMemoryBuffer($transport->getBuffer()); + $readProtocol = new TBinaryProtocol($readTransport); + $result->read($readProtocol); + + $this->assertEquals('only this', $result->stringField); + $this->assertNull($result->intField); + $this->assertNull($result->boolField); + $this->assertNull($result->mapField); + $this->assertNull($result->listField); + $this->assertNull($result->setField); + } + + public function testReadSkipsUnknownField() + { + // Write a struct with field id=99 (unknown to our spec) + $transport = new TMemoryBuffer(); + $protocol = new TBinaryProtocol($transport); + + $protocol->writeStructBegin('Test'); + $protocol->writeFieldBegin('unknown', TType::STRING, 99); + $protocol->writeString('should be skipped'); + $protocol->writeFieldEnd(); + $protocol->writeFieldBegin('stringField', TType::STRING, 1); + $protocol->writeString('known'); + $protocol->writeFieldEnd(); + $protocol->writeFieldStop(); + $protocol->writeStructEnd(); + + $result = new TestRichException(); + $readTransport = new TMemoryBuffer($transport->getBuffer()); + $readProtocol = new TBinaryProtocol($readTransport); + $result->read($readProtocol); + + $this->assertEquals('known', $result->stringField); + } + + public function testReadSkipsMismatchedFieldType() + { + // Write field id=1 as I32 instead of STRING + $transport = new TMemoryBuffer(); + $protocol = new TBinaryProtocol($transport); + + $protocol->writeStructBegin('Test'); + $protocol->writeFieldBegin('stringField', TType::I32, 1); + $protocol->writeI32(999); + $protocol->writeFieldEnd(); + $protocol->writeFieldBegin('intField', TType::I32, 2); + $protocol->writeI32(42); + $protocol->writeFieldEnd(); + $protocol->writeFieldStop(); + $protocol->writeStructEnd(); + + $result = new TestRichException(); + $readTransport = new TMemoryBuffer($transport->getBuffer()); + $readProtocol = new TBinaryProtocol($readTransport); + $result->read($readProtocol); + + $this->assertNull($result->stringField); + $this->assertEquals(42, $result->intField); + } + + private function roundtrip(TestRichException $exception): TestRichException + { + $transport = new TMemoryBuffer(); + $protocol = new TBinaryProtocol($transport); + $exception->write($protocol); + + $result = new TestRichException(); + $readTransport = new TMemoryBuffer($transport->getBuffer()); + $readProtocol = new TBinaryProtocol($readTransport); + $result->read($readProtocol); + + return $result; + } } diff --git a/lib/php/test/Unit/Lib/Factory/TBinaryProtocolFactoryTest.php b/lib/php/test/Unit/Lib/Factory/TBinaryProtocolFactoryTest.php index 04b11598da1..c9f483a5f3d 100644 --- a/lib/php/test/Unit/Lib/Factory/TBinaryProtocolFactoryTest.php +++ b/lib/php/test/Unit/Lib/Factory/TBinaryProtocolFactoryTest.php @@ -71,4 +71,30 @@ public function getProtocolDataProvider() 'strictWrite' => true, ]; } + + /** + * @return void + */ + public function testGetTransport() + { + $transport = $this->createMock(TTransport::class); + $factory = new TBinaryProtocolFactory(); + $protocol = $factory->getProtocol($transport); + + $this->assertSame($transport, $protocol->getTransport()); + } + + /** + * @return void + */ + public function testGetProtocolCreatesNewInstancePerCall() + { + $transport = $this->createMock(TTransport::class); + $factory = new TBinaryProtocolFactory(); + + $protocol1 = $factory->getProtocol($transport); + $protocol2 = $factory->getProtocol($transport); + + $this->assertNotSame($protocol1, $protocol2); + } } diff --git a/lib/php/test/Unit/Lib/Factory/TCompactProtocolFactoryTest.php b/lib/php/test/Unit/Lib/Factory/TCompactProtocolFactoryTest.php index 77fb2ce8593..2c7e6605bd4 100644 --- a/lib/php/test/Unit/Lib/Factory/TCompactProtocolFactoryTest.php +++ b/lib/php/test/Unit/Lib/Factory/TCompactProtocolFactoryTest.php @@ -44,4 +44,30 @@ public function testGetProtocol() $this->assertSame($transport, $this->getPropertyValue($protocol, 'trans_')); } + + /** + * @return void + */ + public function testGetTransport() + { + $transport = $this->createMock(TTransport::class); + $factory = new TCompactProtocolFactory(); + $protocol = $factory->getProtocol($transport); + + $this->assertSame($transport, $protocol->getTransport()); + } + + /** + * @return void + */ + public function testGetProtocolCreatesNewInstancePerCall() + { + $transport = $this->createMock(TTransport::class); + $factory = new TCompactProtocolFactory(); + + $protocol1 = $factory->getProtocol($transport); + $protocol2 = $factory->getProtocol($transport); + + $this->assertNotSame($protocol1, $protocol2); + } } diff --git a/lib/php/test/Unit/Lib/Factory/TFramedTransportFactoryTest.php b/lib/php/test/Unit/Lib/Factory/TFramedTransportFactoryTest.php index 18cdc3ea993..79b67c499a6 100644 --- a/lib/php/test/Unit/Lib/Factory/TFramedTransportFactoryTest.php +++ b/lib/php/test/Unit/Lib/Factory/TFramedTransportFactoryTest.php @@ -46,4 +46,31 @@ public function testGetTransport() $this->assertTrue($this->getPropertyValue($framedTransport, 'write_')); $this->assertSame($transport, $this->getPropertyValue($framedTransport, 'transport_')); } + + /** + * @return void + */ + public function testGetTransportWrapsInnerTransport() + { + $transport = $this->createMock(TTransport::class); + $factory = new TFramedTransportFactory(); + $framedTransport = $factory->getTransport($transport); + + $this->assertNotSame($transport, $framedTransport); + $this->assertInstanceOf(TFramedTransport::class, $framedTransport); + } + + /** + * @return void + */ + public function testGetTransportCreatesNewInstancePerCall() + { + $transport = $this->createMock(TTransport::class); + $factory = new TFramedTransportFactory(); + + $result1 = $factory->getTransport($transport); + $result2 = $factory->getTransport($transport); + + $this->assertNotSame($result1, $result2); + } } diff --git a/lib/php/test/Unit/Lib/Factory/TTransportFactoryTest.php b/lib/php/test/Unit/Lib/Factory/TTransportFactoryTest.php index 9d3e6402ba6..93131d97735 100644 --- a/lib/php/test/Unit/Lib/Factory/TTransportFactoryTest.php +++ b/lib/php/test/Unit/Lib/Factory/TTransportFactoryTest.php @@ -38,4 +38,22 @@ public function testGetTransport() $this->assertSame($transport, $result); } + + /** + * @return void + */ + public function testGetTransportCreatesNewInstancePerCall() + { + $factory = new TTransportFactory(); + + $transport1 = $this->createMock(TTransport::class); + $transport2 = $this->createMock(TTransport::class); + + $this->assertSame($transport1, $factory->getTransport($transport1)); + $this->assertSame($transport2, $factory->getTransport($transport2)); + $this->assertNotSame( + $factory->getTransport($transport1), + $factory->getTransport($transport2) + ); + } } diff --git a/lib/php/test/Unit/Lib/Fixture/ProcessorSpy.php b/lib/php/test/Unit/Lib/Fixture/TestProcessor.php similarity index 97% rename from lib/php/test/Unit/Lib/Fixture/ProcessorSpy.php rename to lib/php/test/Unit/Lib/Fixture/TestProcessor.php index c0005dcca5c..c841a1364d4 100644 --- a/lib/php/test/Unit/Lib/Fixture/ProcessorSpy.php +++ b/lib/php/test/Unit/Lib/Fixture/TestProcessor.php @@ -23,7 +23,7 @@ use Thrift\Protocol\TProtocol; -class ProcessorSpy +class TestProcessor { public function process(TProtocol $input, TProtocol $output) { diff --git a/lib/php/test/Unit/Lib/Fixture/TestRichException.php b/lib/php/test/Unit/Lib/Fixture/TestRichException.php new file mode 100644 index 00000000000..469a4ccbcc9 --- /dev/null +++ b/lib/php/test/Unit/Lib/Fixture/TestRichException.php @@ -0,0 +1,101 @@ + [ + 'var' => 'stringField', + 'type' => TType::STRING, + ], + 2 => [ + 'var' => 'intField', + 'type' => TType::I32, + ], + 3 => [ + 'var' => 'boolField', + 'type' => TType::BOOL, + ], + 4 => [ + 'var' => 'doubleField', + 'type' => TType::DOUBLE, + ], + 5 => [ + 'var' => 'mapField', + 'type' => TType::MAP, + 'ktype' => TType::STRING, + 'vtype' => TType::I32, + 'key' => ['type' => TType::STRING], + 'val' => ['type' => TType::I32], + ], + 6 => [ + 'var' => 'listField', + 'type' => TType::LST, + 'etype' => TType::STRING, + 'elem' => ['type' => TType::STRING], + ], + 7 => [ + 'var' => 'setField', + 'type' => TType::SET, + 'etype' => TType::I32, + 'elem' => ['type' => TType::I32], + ], + ]; + + if (is_array($vals)) { + parent::__construct(self::$_TSPEC, $vals); + } + } + + public function getName() + { + return 'TestRichException'; + } + + public function read($input) + { + return $this->_read(self::class, self::$_TSPEC, $input); + } + + public function write($output) + { + return $this->_write('TestRichException', self::$_TSPEC, $output); + } +} diff --git a/lib/php/test/Unit/Lib/Fixture/TestSerializerStruct.php b/lib/php/test/Unit/Lib/Fixture/TestSerializerStruct.php new file mode 100644 index 00000000000..6b323407f49 --- /dev/null +++ b/lib/php/test/Unit/Lib/Fixture/TestSerializerStruct.php @@ -0,0 +1,57 @@ + [ + 'var' => 'stringField', + 'type' => TType::STRING, + ], + 2 => [ + 'var' => 'intField', + 'type' => TType::I32, + ], + ]; + + public $stringField = null; + public $intField = null; + + public function getName() + { + return 'TestSerializerStruct'; + } + + public function read($input) + { + return $this->_read(self::class, self::$_TSPEC, $input); + } + + public function write($output) + { + return $this->_write('TestSerializerStruct', self::$_TSPEC, $output); + } +} diff --git a/lib/php/test/Unit/Lib/Protocol/BoundaryValuesTest.php b/lib/php/test/Unit/Lib/Protocol/BoundaryValuesTest.php new file mode 100644 index 00000000000..7d53d436418 --- /dev/null +++ b/lib/php/test/Unit/Lib/Protocol/BoundaryValuesTest.php @@ -0,0 +1,138 @@ + TType::BYTE, + 'writeI16' => TType::I16, + 'writeI32' => TType::I32, + 'writeI64' => TType::I64, + 'writeDouble' => TType::DOUBLE, + 'writeString' => TType::STRING, + 'writeBool' => TType::BOOL, + ]; + + /** + * @dataProvider boundaryProvider + */ + public function testBoundaryValues($protocolClass, $writeMethod, $readMethod, $value) + { + $this->assertRoundtrip($protocolClass, $writeMethod, $readMethod, $value); + } + + public static function boundaryProvider(): array + { + $cases = [ + // integers + 'byte min' => ['writeMethod' => 'writeByte', 'readMethod' => 'readByte', 'value' => -128], + 'byte max' => ['writeMethod' => 'writeByte', 'readMethod' => 'readByte', 'value' => 127], + 'byte zero' => ['writeMethod' => 'writeByte', 'readMethod' => 'readByte', 'value' => 0], + 'i16 min' => ['writeMethod' => 'writeI16', 'readMethod' => 'readI16', 'value' => -32768], + 'i16 max' => ['writeMethod' => 'writeI16', 'readMethod' => 'readI16', 'value' => 32767], + 'i16 zero' => ['writeMethod' => 'writeI16', 'readMethod' => 'readI16', 'value' => 0], + 'i32 min' => ['writeMethod' => 'writeI32', 'readMethod' => 'readI32', 'value' => -2147483648], + 'i32 max' => ['writeMethod' => 'writeI32', 'readMethod' => 'readI32', 'value' => 2147483647], + 'i32 zero' => ['writeMethod' => 'writeI32', 'readMethod' => 'readI32', 'value' => 0], + 'i64 min' => ['writeMethod' => 'writeI64', 'readMethod' => 'readI64', 'value' => PHP_INT_MIN], + 'i64 max' => ['writeMethod' => 'writeI64', 'readMethod' => 'readI64', 'value' => PHP_INT_MAX], + 'i64 zero' => ['writeMethod' => 'writeI64', 'readMethod' => 'readI64', 'value' => 0], + + // doubles + 'double zero' => ['writeMethod' => 'writeDouble', 'readMethod' => 'readDouble', 'value' => 0.0], + 'double negative zero' => ['writeMethod' => 'writeDouble', 'readMethod' => 'readDouble', 'value' => -0.0], + // TODO: replace literals with PHP_FLOAT_MAX/MIN/EPSILON when PHP 7.1 support is dropped (available since PHP 7.2) + 'double max' => ['writeMethod' => 'writeDouble', 'readMethod' => 'readDouble', 'value' => 1.7976931348623158e+308], + 'double min' => ['writeMethod' => 'writeDouble', 'readMethod' => 'readDouble', 'value' => 2.2250738585072014e-308], + 'double epsilon' => ['writeMethod' => 'writeDouble', 'readMethod' => 'readDouble', 'value' => 2.2204460492503131e-16], + 'double very small' => ['writeMethod' => 'writeDouble', 'readMethod' => 'readDouble', 'value' => 1e-300], + + // strings + 'empty string' => ['writeMethod' => 'writeString', 'readMethod' => 'readString', 'value' => ''], + 'null byte' => ['writeMethod' => 'writeString', 'readMethod' => 'readString', 'value' => "\x00"], + 'unicode' => ['writeMethod' => 'writeString', 'readMethod' => 'readString', 'value' => "Привіт 🌍"], + 'long string' => ['writeMethod' => 'writeString', 'readMethod' => 'readString', 'value' => str_repeat('a', 1024)], + + // booleans + 'bool true' => ['writeMethod' => 'writeBool', 'readMethod' => 'readBool', 'value' => true], + 'bool false' => ['writeMethod' => 'writeBool', 'readMethod' => 'readBool', 'value' => false], + ]; + + return self::expandWithProtocols($cases); + } + + private static function expandWithProtocols(array $cases): array + { + $protocols = [ + 'Binary' => TBinaryProtocol::class, + 'Compact' => TCompactProtocol::class, + 'JSON' => TJSONProtocol::class, + ]; + + $expanded = []; + foreach ($protocols as $protoName => $protoClass) { + foreach ($cases as $caseName => $case) { + $expanded[$protoName . ' ' . $caseName] = [ + 'protocolClass' => $protoClass, + 'writeMethod' => $case['writeMethod'], + 'readMethod' => $case['readMethod'], + 'value' => $case['value'], + ]; + } + } + + return $expanded; + } + + private function assertRoundtrip($protocolClass, $writeMethod, $readMethod, $value) + { + $fieldType = self::$WRITE_TYPE_MAP[$writeMethod]; + $transport = new TMemoryBuffer(); + $protocol = new $protocolClass($transport); + + $protocol->writeStructBegin('Test'); + $protocol->writeFieldBegin('field', $fieldType, 1); + $protocol->$writeMethod($value); + $protocol->writeFieldEnd(); + $protocol->writeFieldStop(); + $protocol->writeStructEnd(); + + $transport2 = new TMemoryBuffer($transport->getBuffer()); + $protocol2 = new $protocolClass($transport2); + $protocol2->readStructBegin($name); + $protocol2->readFieldBegin($fname, $ftype, $fid); + $protocol2->$readMethod($result); + $protocol2->readFieldEnd(); + $protocol2->readStructEnd(); + + $this->assertEquals($value, $result); + } +} diff --git a/lib/php/test/Unit/Lib/Protocol/TBinaryProtocolTest.php b/lib/php/test/Unit/Lib/Protocol/TBinaryProtocolTest.php index c89555cee57..d81be374b06 100644 --- a/lib/php/test/Unit/Lib/Protocol/TBinaryProtocolTest.php +++ b/lib/php/test/Unit/Lib/Protocol/TBinaryProtocolTest.php @@ -874,6 +874,12 @@ public function testReadI64For32BitArchitecture( public function readI64For32BitArchitectureDataProvider() { $storedValueRepresent = function ($value) { + // PHP_INT_MIN (-2^63) cannot be safely negated: + // -PHP_INT_MIN overflows the 64-bit signed integer range. + if ($value === PHP_INT_MIN) { + return pack('N2', 0x80000000, 0x00000000); + } + $neg = $value < 0; if ($neg) { diff --git a/lib/php/test/Unit/Lib/Protocol/TJSONProtocolTest.php b/lib/php/test/Unit/Lib/Protocol/TJSONProtocolTest.php new file mode 100644 index 00000000000..f0499fd8d6a --- /dev/null +++ b/lib/php/test/Unit/Lib/Protocol/TJSONProtocolTest.php @@ -0,0 +1,709 @@ +writeMessageBegin($name, $type, $seqid); + $protocol->writeMessageEnd(); + + $protocol->reset(); + $transport = new TMemoryBuffer($transport->getBuffer()); + $protocol = new TJSONProtocol($transport); + + $readName = null; + $readType = null; + $readSeqid = null; + $protocol->readMessageBegin($readName, $readType, $readSeqid); + $protocol->readMessageEnd(); + + $this->assertSame($name, $readName); + $this->assertSame($type, $readType); + $this->assertSame($seqid, $readSeqid); + } + + public function writeAndReadMessageBeginDataProvider() + { + yield 'call message' => [ + 'name' => 'testMethod', + 'type' => TMessageType::CALL, + 'seqid' => 1, + ]; + yield 'reply message' => [ + 'name' => 'getResult', + 'type' => TMessageType::REPLY, + 'seqid' => 42, + ]; + yield 'exception message' => [ + 'name' => 'failMethod', + 'type' => TMessageType::EXCEPTION, + 'seqid' => 100, + ]; + yield 'oneway message' => [ + 'name' => 'fireAndForget', + 'type' => TMessageType::ONEWAY, + 'seqid' => 0, + ]; + } + + public function testReadMessageBeginBadVersion() + { + // Manually craft a JSON message with wrong version (99 instead of 1) + $json = '[99,"testMethod",1,1]'; + $transport = new TMemoryBuffer($json); + $protocol = new TJSONProtocol($transport); + + $this->expectException(TProtocolException::class); + $this->expectExceptionCode(TProtocolException::BAD_VERSION); + + $name = null; + $type = null; + $seqid = null; + $protocol->readMessageBegin($name, $type, $seqid); + } + + /** + * @dataProvider writeAndReadScalarDataProvider + */ + public function testWriteAndReadScalar(int $fieldType, string $writeMethod, $value, string $readMethod) + { + if ($fieldType === TType::I64 && PHP_INT_SIZE === 4) { + $this->markTestSkipped('64-bit integer tests require 64-bit PHP'); + } + + $transport = new TMemoryBuffer(); + $protocol = new TJSONProtocol($transport); + + $protocol->writeStructBegin('Test'); + $protocol->writeFieldBegin('f', $fieldType, 1); + $protocol->$writeMethod($value); + $protocol->writeFieldEnd(); + $protocol->writeFieldStop(); + $protocol->writeStructEnd(); + + $transport = new TMemoryBuffer($transport->getBuffer()); + $protocol = new TJSONProtocol($transport); + + $protocol->readStructBegin($name); + $protocol->readFieldBegin($fname, $ftype, $fid); + $result = null; + $protocol->$readMethod($result); + $protocol->readFieldEnd(); + $protocol->readStructEnd(); + + if (is_float($value) && is_nan($value)) { + $this->assertNan($result); + } else { + $this->assertSame($value, $result); + } + } + + public function writeAndReadScalarDataProvider() + { + yield 'bool true' => [ + 'fieldType' => TType::BOOL, + 'writeMethod' => 'writeBool', + 'value' => true, + 'readMethod' => 'readBool', + ]; + yield 'bool false' => [ + 'fieldType' => TType::BOOL, + 'writeMethod' => 'writeBool', + 'value' => false, + 'readMethod' => 'readBool', + ]; + yield 'byte zero' => [ + 'fieldType' => TType::BYTE, + 'writeMethod' => 'writeByte', + 'value' => 0, + 'readMethod' => 'readByte', + ]; + yield 'byte positive' => [ + 'fieldType' => TType::BYTE, + 'writeMethod' => 'writeByte', + 'value' => 127, + 'readMethod' => 'readByte', + ]; + yield 'byte negative' => [ + 'fieldType' => TType::BYTE, + 'writeMethod' => 'writeByte', + 'value' => -128, + 'readMethod' => 'readByte', + ]; + yield 'byte one' => [ + 'fieldType' => TType::BYTE, + 'writeMethod' => 'writeByte', + 'value' => 1, + 'readMethod' => 'readByte', + ]; + yield 'i16 zero' => [ + 'fieldType' => TType::I16, + 'writeMethod' => 'writeI16', + 'value' => 0, + 'readMethod' => 'readI16', + ]; + yield 'i16 positive' => [ + 'fieldType' => TType::I16, + 'writeMethod' => 'writeI16', + 'value' => 32767, + 'readMethod' => 'readI16', + ]; + yield 'i16 negative' => [ + 'fieldType' => TType::I16, + 'writeMethod' => 'writeI16', + 'value' => -32768, + 'readMethod' => 'readI16', + ]; + yield 'i16 small positive' => [ + 'fieldType' => TType::I16, + 'writeMethod' => 'writeI16', + 'value' => 256, + 'readMethod' => 'readI16', + ]; + yield 'i32 zero' => [ + 'fieldType' => TType::I32, + 'writeMethod' => 'writeI32', + 'value' => 0, + 'readMethod' => 'readI32', + ]; + yield 'i32 positive' => [ + 'fieldType' => TType::I32, + 'writeMethod' => 'writeI32', + 'value' => 2147483647, + 'readMethod' => 'readI32', + ]; + yield 'i32 negative' => [ + 'fieldType' => TType::I32, + 'writeMethod' => 'writeI32', + 'value' => -2147483648, + 'readMethod' => 'readI32', + ]; + yield 'i32 small negative' => [ + 'fieldType' => TType::I32, + 'writeMethod' => 'writeI32', + 'value' => -1, + 'readMethod' => 'readI32', + ]; + yield 'i32 medium value' => [ + 'fieldType' => TType::I32, + 'writeMethod' => 'writeI32', + 'value' => 100000, + 'readMethod' => 'readI32', + ]; + yield 'i64 zero' => [ + 'fieldType' => TType::I64, + 'writeMethod' => 'writeI64', + 'value' => 0, + 'readMethod' => 'readI64', + ]; + yield 'i64 positive' => [ + 'fieldType' => TType::I64, + 'writeMethod' => 'writeI64', + 'value' => 1099511627776, + 'readMethod' => 'readI64', + ]; + yield 'i64 negative' => [ + 'fieldType' => TType::I64, + 'writeMethod' => 'writeI64', + 'value' => -1099511627776, + 'readMethod' => 'readI64', + ]; + yield 'i64 max int32' => [ + 'fieldType' => TType::I64, + 'writeMethod' => 'writeI64', + 'value' => 2147483647, + 'readMethod' => 'readI64', + ]; + yield 'i64 small value' => [ + 'fieldType' => TType::I64, + 'writeMethod' => 'writeI64', + 'value' => 42, + 'readMethod' => 'readI64', + ]; + yield 'double zero' => [ + 'fieldType' => TType::DOUBLE, + 'writeMethod' => 'writeDouble', + 'value' => 0.0, + 'readMethod' => 'readDouble', + ]; + yield 'double positive' => [ + 'fieldType' => TType::DOUBLE, + 'writeMethod' => 'writeDouble', + 'value' => 3.14159265358979, + 'readMethod' => 'readDouble', + ]; + yield 'double negative' => [ + 'fieldType' => TType::DOUBLE, + 'writeMethod' => 'writeDouble', + 'value' => -2.718281828, + 'readMethod' => 'readDouble', + ]; + yield 'double large' => [ + 'fieldType' => TType::DOUBLE, + 'writeMethod' => 'writeDouble', + 'value' => 1.7976931348623e+100, + 'readMethod' => 'readDouble', + ]; + yield 'double small' => [ + 'fieldType' => TType::DOUBLE, + 'writeMethod' => 'writeDouble', + 'value' => 1.0e-10, + 'readMethod' => 'readDouble', + ]; + yield 'string empty' => [ + 'fieldType' => TType::STRING, + 'writeMethod' => 'writeString', + 'value' => '', + 'readMethod' => 'readString', + ]; + yield 'string simple' => [ + 'fieldType' => TType::STRING, + 'writeMethod' => 'writeString', + 'value' => 'hello world', + 'readMethod' => 'readString', + ]; + yield 'string special characters' => [ + 'fieldType' => TType::STRING, + 'writeMethod' => 'writeString', + 'value' => "line1\nline2\ttab", + 'readMethod' => 'readString', + ]; + yield 'string unicode' => [ + 'fieldType' => TType::STRING, + 'writeMethod' => 'writeString', + 'value' => 'héllo wörld', + 'readMethod' => 'readString', + ]; + yield 'string quotes and backslash' => [ + 'fieldType' => TType::STRING, + 'writeMethod' => 'writeString', + 'value' => 'say "hello" and use \\path', + 'readMethod' => 'readString', + ]; + yield 'string json special chars' => [ + 'fieldType' => TType::STRING, + 'writeMethod' => 'writeString', + 'value' => '{"key": "value"}', + 'readMethod' => 'readString', + ]; + yield 'uuid standard' => [ + 'fieldType' => TType::UUID, + 'writeMethod' => 'writeUuid', + 'value' => '12345678-1234-5678-1234-567812345678', + 'readMethod' => 'readUuid', + ]; + yield 'uuid nil' => [ + 'fieldType' => TType::UUID, + 'writeMethod' => 'writeUuid', + 'value' => '00000000-0000-0000-0000-000000000000', + 'readMethod' => 'readUuid', + ]; + yield 'uuid random' => [ + 'fieldType' => TType::UUID, + 'writeMethod' => 'writeUuid', + 'value' => 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + 'readMethod' => 'readUuid', + ]; + } + + public function testWriteAndReadStruct() + { + $transport = new TMemoryBuffer(); + $protocol = new TJSONProtocol($transport); + + $protocol->writeStructBegin('TestStruct'); + $protocol->writeFieldBegin('name', TType::STRING, 1); + $protocol->writeString('hello'); + $protocol->writeFieldEnd(); + $protocol->writeFieldBegin('age', TType::I32, 2); + $protocol->writeI32(25); + $protocol->writeFieldEnd(); + $protocol->writeFieldStop(); + $protocol->writeStructEnd(); + + $transport = new TMemoryBuffer($transport->getBuffer()); + $protocol = new TJSONProtocol($transport); + + $name = null; + $protocol->readStructBegin($name); + + $fieldName = null; + $fieldType = null; + $fieldId = null; + $protocol->readFieldBegin($fieldName, $fieldType, $fieldId); + $this->assertSame(TType::STRING, $fieldType); + $this->assertSame(1, $fieldId); + $str = null; + $protocol->readString($str); + $this->assertSame('hello', $str); + $protocol->readFieldEnd(); + + $protocol->readFieldBegin($fieldName, $fieldType, $fieldId); + $this->assertSame(TType::I32, $fieldType); + $this->assertSame(2, $fieldId); + $i32 = null; + $protocol->readI32($i32); + $this->assertSame(25, $i32); + $protocol->readFieldEnd(); + + // Read field stop + $protocol->readFieldBegin($fieldName, $fieldType, $fieldId); + $this->assertSame(TType::STOP, $fieldType); + + $protocol->readStructEnd(); + } + + public function testWriteAndReadMap() + { + $transport = new TMemoryBuffer(); + $protocol = new TJSONProtocol($transport); + + $protocol->writeMapBegin(TType::STRING, TType::I32, 2); + $protocol->writeString('key1'); + $protocol->writeI32(100); + $protocol->writeString('key2'); + $protocol->writeI32(200); + $protocol->writeMapEnd(); + + $transport = new TMemoryBuffer($transport->getBuffer()); + $protocol = new TJSONProtocol($transport); + + $keyType = null; + $valType = null; + $size = null; + $protocol->readMapBegin($keyType, $valType, $size); + $this->assertSame(TType::STRING, $keyType); + $this->assertSame(TType::I32, $valType); + $this->assertSame(2, $size); + + $key = null; + $val = null; + $protocol->readString($key); + $protocol->readI32($val); + $this->assertSame('key1', $key); + $this->assertSame(100, $val); + + $protocol->readString($key); + $protocol->readI32($val); + $this->assertSame('key2', $key); + $this->assertSame(200, $val); + + $protocol->readMapEnd(); + } + + public function testWriteAndReadList() + { + $transport = new TMemoryBuffer(); + $protocol = new TJSONProtocol($transport); + + $protocol->writeListBegin(TType::I32, 3); + $protocol->writeI32(10); + $protocol->writeI32(20); + $protocol->writeI32(30); + $protocol->writeListEnd(); + + $transport = new TMemoryBuffer($transport->getBuffer()); + $protocol = new TJSONProtocol($transport); + + $elemType = null; + $size = null; + $protocol->readListBegin($elemType, $size); + $this->assertSame(TType::I32, $elemType); + $this->assertSame(3, $size); + + $val = null; + $protocol->readI32($val); + $this->assertSame(10, $val); + $protocol->readI32($val); + $this->assertSame(20, $val); + $protocol->readI32($val); + $this->assertSame(30, $val); + + $protocol->readListEnd(); + } + + public function testWriteAndReadSet() + { + $transport = new TMemoryBuffer(); + $protocol = new TJSONProtocol($transport); + + $protocol->writeSetBegin(TType::STRING, 2); + $protocol->writeString('alpha'); + $protocol->writeString('beta'); + $protocol->writeSetEnd(); + + $transport = new TMemoryBuffer($transport->getBuffer()); + $protocol = new TJSONProtocol($transport); + + $elemType = null; + $size = null; + $protocol->readSetBegin($elemType, $size); + $this->assertSame(TType::STRING, $elemType); + $this->assertSame(2, $size); + + $val = null; + $protocol->readString($val); + $this->assertSame('alpha', $val); + $protocol->readString($val); + $this->assertSame('beta', $val); + + $protocol->readSetEnd(); + } + + public function testWriteAndReadEmptyMap() + { + $transport = new TMemoryBuffer(); + $protocol = new TJSONProtocol($transport); + + $protocol->writeMapBegin(TType::STRING, TType::I32, 0); + $protocol->writeMapEnd(); + + $transport = new TMemoryBuffer($transport->getBuffer()); + $protocol = new TJSONProtocol($transport); + + $keyType = null; + $valType = null; + $size = null; + $protocol->readMapBegin($keyType, $valType, $size); + $this->assertSame(TType::STRING, $keyType); + $this->assertSame(TType::I32, $valType); + $this->assertSame(0, $size); + + $protocol->readMapEnd(); + } + + public function testWriteAndReadEmptyList() + { + $transport = new TMemoryBuffer(); + $protocol = new TJSONProtocol($transport); + + $protocol->writeListBegin(TType::BOOL, 0); + $protocol->writeListEnd(); + + $transport = new TMemoryBuffer($transport->getBuffer()); + $protocol = new TJSONProtocol($transport); + + $elemType = null; + $size = null; + $protocol->readListBegin($elemType, $size); + $this->assertSame(TType::BOOL, $elemType); + $this->assertSame(0, $size); + + $protocol->readListEnd(); + } + + public function testGetTypeNameForUnknownType() + { + $transport = new TMemoryBuffer(); + $protocol = new TJSONProtocol($transport); + + $this->expectException(TProtocolException::class); + $this->expectExceptionCode(TProtocolException::UNKNOWN); + + // Use writeFieldBegin with an invalid type to trigger getTypeNameForTypeID + $protocol->writeFieldBegin('invalid', 99, 1); + } + + public function testGetTypeIDForUnknownTypeName() + { + // Craft JSON that has an unknown type name in field position + // A struct with one field: fieldId=1, then object start, then unknown type name + $json = '{1:{"zzz"'; + $transport = new TMemoryBuffer($json); + $protocol = new TJSONProtocol($transport); + + $this->expectException(TProtocolException::class); + $this->expectExceptionCode(TProtocolException::INVALID_DATA); + + $name = null; + $protocol->readStructBegin($name); + + $fieldName = null; + $fieldType = null; + $fieldId = null; + $protocol->readFieldBegin($fieldName, $fieldType, $fieldId); + } + + public function testWriteAndReadCompleteMessage() + { + $transport = new TMemoryBuffer(); + $protocol = new TJSONProtocol($transport); + + // Write a complete message with a struct containing various types + $protocol->writeMessageBegin('testFunc', TMessageType::CALL, 7); + $protocol->writeStructBegin('Args'); + $protocol->writeFieldBegin('flag', TType::BOOL, 1); + $protocol->writeBool(true); + $protocol->writeFieldEnd(); + $protocol->writeFieldBegin('count', TType::I32, 2); + $protocol->writeI32(42); + $protocol->writeFieldEnd(); + $protocol->writeFieldStop(); + $protocol->writeStructEnd(); + $protocol->writeMessageEnd(); + + $transport = new TMemoryBuffer($transport->getBuffer()); + $protocol = new TJSONProtocol($transport); + + $name = null; + $type = null; + $seqid = null; + $protocol->readMessageBegin($name, $type, $seqid); + $this->assertSame('testFunc', $name); + $this->assertSame(TMessageType::CALL, $type); + $this->assertSame(7, $seqid); + + $structName = null; + $protocol->readStructBegin($structName); + + $fieldName = null; + $fieldType = null; + $fieldId = null; + + $protocol->readFieldBegin($fieldName, $fieldType, $fieldId); + $this->assertSame(TType::BOOL, $fieldType); + $this->assertSame(1, $fieldId); + $boolVal = null; + $protocol->readBool($boolVal); + $this->assertTrue($boolVal); + $protocol->readFieldEnd(); + + $protocol->readFieldBegin($fieldName, $fieldType, $fieldId); + $this->assertSame(TType::I32, $fieldType); + $this->assertSame(2, $fieldId); + $i32Val = null; + $protocol->readI32($i32Val); + $this->assertSame(42, $i32Val); + $protocol->readFieldEnd(); + + $protocol->readFieldBegin($fieldName, $fieldType, $fieldId); + $this->assertSame(TType::STOP, $fieldType); + + $protocol->readStructEnd(); + $protocol->readMessageEnd(); + } + + public function testWriteAndReadNestedList() + { + $transport = new TMemoryBuffer(); + $protocol = new TJSONProtocol($transport); + + // Write a list of lists + $protocol->writeListBegin(TType::LST, 2); + + $protocol->writeListBegin(TType::I32, 2); + $protocol->writeI32(1); + $protocol->writeI32(2); + $protocol->writeListEnd(); + + $protocol->writeListBegin(TType::I32, 2); + $protocol->writeI32(3); + $protocol->writeI32(4); + $protocol->writeListEnd(); + + $protocol->writeListEnd(); + + $transport = new TMemoryBuffer($transport->getBuffer()); + $protocol = new TJSONProtocol($transport); + + $elemType = null; + $size = null; + $protocol->readListBegin($elemType, $size); + $this->assertSame(TType::LST, $elemType); + $this->assertSame(2, $size); + + $protocol->readListBegin($elemType, $size); + $this->assertSame(TType::I32, $elemType); + $this->assertSame(2, $size); + $val = null; + $protocol->readI32($val); + $this->assertSame(1, $val); + $protocol->readI32($val); + $this->assertSame(2, $val); + $protocol->readListEnd(); + + $protocol->readListBegin($elemType, $size); + $this->assertSame(TType::I32, $elemType); + $this->assertSame(2, $size); + $protocol->readI32($val); + $this->assertSame(3, $val); + $protocol->readI32($val); + $this->assertSame(4, $val); + $protocol->readListEnd(); + + $protocol->readListEnd(); + } + + public function testWriteAndReadMapWithIntegerKeys() + { + $transport = new TMemoryBuffer(); + $protocol = new TJSONProtocol($transport); + + $protocol->writeMapBegin(TType::I32, TType::STRING, 2); + $protocol->writeI32(1); + $protocol->writeString('one'); + $protocol->writeI32(2); + $protocol->writeString('two'); + $protocol->writeMapEnd(); + + $transport = new TMemoryBuffer($transport->getBuffer()); + $protocol = new TJSONProtocol($transport); + + $keyType = null; + $valType = null; + $size = null; + $protocol->readMapBegin($keyType, $valType, $size); + $this->assertSame(TType::I32, $keyType); + $this->assertSame(TType::STRING, $valType); + $this->assertSame(2, $size); + + $key = null; + $val = null; + $protocol->readI32($key); + $protocol->readString($val); + $this->assertSame(1, $key); + $this->assertSame('one', $val); + + $protocol->readI32($key); + $protocol->readString($val); + $this->assertSame(2, $key); + $this->assertSame('two', $val); + + $protocol->readMapEnd(); + } +} diff --git a/lib/php/test/Unit/Lib/Protocol/TProtocolDecoratorTest.php b/lib/php/test/Unit/Lib/Protocol/TProtocolDecoratorTest.php index e5dbdb36b29..0b34a5b6a1e 100644 --- a/lib/php/test/Unit/Lib/Protocol/TProtocolDecoratorTest.php +++ b/lib/php/test/Unit/Lib/Protocol/TProtocolDecoratorTest.php @@ -92,5 +92,7 @@ public function methodDecorationDataProvider() yield 'readI64' => ['readI64', ['value']]; yield 'readDouble' => ['readDouble', ['value']]; yield 'readString' => ['readString', ['value']]; + yield 'writeUuid' => ['writeUuid', ['value']]; + yield 'readUuid' => ['readUuid', ['value']]; } } diff --git a/lib/php/test/Unit/Lib/Protocol/TSimpleJSONProtocolTest.php b/lib/php/test/Unit/Lib/Protocol/TSimpleJSONProtocolTest.php index e4d005a45ae..ca0c081d2b2 100644 --- a/lib/php/test/Unit/Lib/Protocol/TSimpleJSONProtocolTest.php +++ b/lib/php/test/Unit/Lib/Protocol/TSimpleJSONProtocolTest.php @@ -25,6 +25,8 @@ use PHPUnit\Framework\TestCase; use Thrift\Exception\TException; use Thrift\Protocol\TSimpleJSONProtocol; +use Thrift\Protocol\SimpleJSON\CollectionMapKeyException; +use Thrift\Transport\TMemoryBuffer; use Thrift\Transport\TTransport; use Thrift\Type\TType; @@ -130,4 +132,289 @@ public function readDataProvider() 'methodArguments' => ['string'], ]; } + + /** + * @dataProvider writeScalarProvider + */ + public function testWriteScalar(string $writeMethod, $value, string $expectedJson) + { + $transport = new TMemoryBuffer(); + $protocol = new TSimpleJSONProtocol($transport); + + $protocol->writeListBegin(TType::STRING, 1); + $protocol->$writeMethod($value); + $protocol->writeListEnd(); + + $this->assertSame('[' . $expectedJson . ']', $transport->getBuffer()); + } + + public function writeScalarProvider() + { + yield 'bool true' => [ + 'writeMethod' => 'writeBool', + 'value' => true, + 'expectedJson' => '1', + ]; + yield 'bool false' => [ + 'writeMethod' => 'writeBool', + 'value' => false, + 'expectedJson' => '0', + ]; + yield 'byte zero' => [ + 'writeMethod' => 'writeByte', + 'value' => 0, + 'expectedJson' => '0', + ]; + yield 'byte max' => [ + 'writeMethod' => 'writeByte', + 'value' => 127, + 'expectedJson' => '127', + ]; + yield 'byte min' => [ + 'writeMethod' => 'writeByte', + 'value' => -128, + 'expectedJson' => '-128', + ]; + yield 'i16 max' => [ + 'writeMethod' => 'writeI16', + 'value' => 32767, + 'expectedJson' => '32767', + ]; + yield 'i16 min' => [ + 'writeMethod' => 'writeI16', + 'value' => -32768, + 'expectedJson' => '-32768', + ]; + yield 'i16 zero' => [ + 'writeMethod' => 'writeI16', + 'value' => 0, + 'expectedJson' => '0', + ]; + yield 'i32 max' => [ + 'writeMethod' => 'writeI32', + 'value' => 2147483647, + 'expectedJson' => '2147483647', + ]; + yield 'i32 zero' => [ + 'writeMethod' => 'writeI32', + 'value' => 0, + 'expectedJson' => '0', + ]; + yield 'i32 negative' => [ + 'writeMethod' => 'writeI32', + 'value' => -1, + 'expectedJson' => '-1', + ]; + yield 'i64 large' => [ + 'writeMethod' => 'writeI64', + 'value' => 1000000000, + 'expectedJson' => '1000000000', + ]; + yield 'i64 zero' => [ + 'writeMethod' => 'writeI64', + 'value' => 0, + 'expectedJson' => '0', + ]; + yield 'i64 negative' => [ + 'writeMethod' => 'writeI64', + 'value' => -1000000000, + 'expectedJson' => '-1000000000', + ]; + yield 'double pi' => [ + 'writeMethod' => 'writeDouble', + 'value' => 3.14, + 'expectedJson' => json_encode(3.14), + ]; + yield 'double negative' => [ + 'writeMethod' => 'writeDouble', + 'value' => -2.5, + 'expectedJson' => json_encode(-2.5), + ]; + yield 'double large' => [ + 'writeMethod' => 'writeDouble', + 'value' => 1.0e10, + 'expectedJson' => json_encode(1.0e10), + ]; + yield 'double zero' => [ + 'writeMethod' => 'writeDouble', + 'value' => 0.0, + 'expectedJson' => json_encode(0.0), + ]; + yield 'string simple' => [ + 'writeMethod' => 'writeString', + 'value' => 'hello', + 'expectedJson' => '"hello"', + ]; + yield 'string with quotes' => [ + 'writeMethod' => 'writeString', + 'value' => 'quote "inside"', + 'expectedJson' => '"quote \"inside\""', + ]; + yield 'string with path' => [ + 'writeMethod' => 'writeString', + 'value' => 'path/to/file', + 'expectedJson' => '"path/to/file"', + ]; + yield 'string empty' => [ + 'writeMethod' => 'writeString', + 'value' => '', + 'expectedJson' => '""', + ]; + yield 'string unicode' => [ + 'writeMethod' => 'writeString', + 'value' => 'привіт', + 'expectedJson' => json_encode('привіт'), + ]; + yield 'uuid' => [ + 'writeMethod' => 'writeUuid', + 'value' => '12345678-1234-5678-1234-567812345678', + 'expectedJson' => '"12345678-1234-5678-1234-567812345678"', + ]; + } + + /** + * @dataProvider writeContainerProvider + */ + public function testWriteContainer(array $operations, string $expectedJson) + { + $transport = new TMemoryBuffer(); + $protocol = new TSimpleJSONProtocol($transport); + + foreach ($operations as [$method, $args]) { + $protocol->$method(...$args); + } + + $this->assertSame($expectedJson, $transport->getBuffer()); + } + + public function writeContainerProvider() + { + yield 'list of integers' => [ + 'operations' => [ + ['writeListBegin', [TType::I32, 3]], + ['writeI32', [1]], + ['writeI32', [2]], + ['writeI32', [3]], + ['writeListEnd', []], + ], + 'expectedJson' => '[1,2,3]', + ]; + yield 'empty list' => [ + 'operations' => [ + ['writeListBegin', [TType::I32, 0]], + ['writeListEnd', []], + ], + 'expectedJson' => '[]', + ]; + yield 'set of integers' => [ + 'operations' => [ + ['writeSetBegin', [TType::I32, 2]], + ['writeI32', [10]], + ['writeI32', [20]], + ['writeSetEnd', []], + ], + 'expectedJson' => '[10,20]', + ]; + yield 'map string to i32' => [ + 'operations' => [ + ['writeMapBegin', [TType::STRING, TType::I32, 2]], + ['writeString', ['key1']], + ['writeI32', [100]], + ['writeString', ['key2']], + ['writeI32', [200]], + ['writeMapEnd', []], + ], + 'expectedJson' => '{"key1":100,"key2":200}', + ]; + yield 'empty map' => [ + 'operations' => [ + ['writeMapBegin', [TType::STRING, TType::I32, 0]], + ['writeMapEnd', []], + ], + 'expectedJson' => '{}', + ]; + yield 'nested list' => [ + 'operations' => [ + ['writeListBegin', [TType::LST, 2]], + ['writeListBegin', [TType::I32, 2]], + ['writeI32', [1]], + ['writeI32', [2]], + ['writeListEnd', []], + ['writeListBegin', [TType::I32, 2]], + ['writeI32', [3]], + ['writeI32', [4]], + ['writeListEnd', []], + ['writeListEnd', []], + ], + 'expectedJson' => '[[1,2],[3,4]]', + ]; + } + + /** + * @dataProvider writeStructuralProvider + */ + public function testWriteStructural(array $operations, string $expectedJson) + { + $transport = new TMemoryBuffer(); + $protocol = new TSimpleJSONProtocol($transport); + + foreach ($operations as [$method, $args]) { + $protocol->$method(...$args); + } + + $this->assertSame($expectedJson, $transport->getBuffer()); + } + + public function writeStructuralProvider() + { + yield 'message begin' => [ + 'operations' => [ + ['writeMessageBegin', ['name', 1, 42]], + ], + 'expectedJson' => '["name",1,42', + ]; + yield 'message begin and end' => [ + 'operations' => [ + ['writeMessageBegin', ['name', 1, 42]], + ['writeMessageEnd', []], + ], + 'expectedJson' => '["name",1,42]', + ]; + yield 'struct with i32 field' => [ + 'operations' => [ + ['writeStructBegin', ['MyStruct']], + ['writeFieldBegin', ['field_name', TType::I32, 1]], + ['writeI32', [42]], + ['writeFieldEnd', []], + ['writeFieldStop', []], + ['writeStructEnd', []], + ], + 'expectedJson' => '{"field_name":42}', + ]; + yield 'struct with multiple fields' => [ + 'operations' => [ + ['writeStructBegin', ['MyStruct']], + ['writeFieldBegin', ['name', TType::STRING, 1]], + ['writeString', ['test']], + ['writeFieldEnd', []], + ['writeFieldBegin', ['value', TType::I32, 2]], + ['writeI32', [99]], + ['writeFieldEnd', []], + ['writeFieldStop', []], + ['writeStructEnd', []], + ], + 'expectedJson' => '{"name":"test","value":99}', + ]; + } + + public function testMapKeyCannotBeCollection() + { + $this->expectException(CollectionMapKeyException::class); + + $transport = new TMemoryBuffer(); + $protocol = new TSimpleJSONProtocol($transport); + + $protocol->writeMapBegin(TType::STRING, TType::I32, 1); + $protocol->writeMapBegin(TType::STRING, TType::I32, 1); + } } diff --git a/lib/php/test/Unit/Lib/ReflectionHelper.php b/lib/php/test/Unit/Lib/ReflectionHelper.php index 5db87415ff6..1370a27a135 100644 --- a/lib/php/test/Unit/Lib/ReflectionHelper.php +++ b/lib/php/test/Unit/Lib/ReflectionHelper.php @@ -23,6 +23,25 @@ trait ReflectionHelper { + /** + * Get a reflection method and make it accessible if needed + * + * @param object|string $objectOrClass + * @param string $methodName + * @return \ReflectionMethod + */ + protected function getAccessibleMethod($objectOrClass, string $methodName): \ReflectionMethod + { + $method = new \ReflectionMethod($objectOrClass, $methodName); + + // Only call setAccessible for PHP < 8.1.0 + if (PHP_VERSION_ID < 80100) { + $method->setAccessible(true); + } + + return $method; + } + /** * Get a reflection property and make it accessible if needed * diff --git a/lib/php/test/Unit/Lib/Serializer/TBinarySerializerTest.php b/lib/php/test/Unit/Lib/Serializer/TBinarySerializerTest.php index 292f261892c..fb17c1a6102 100644 --- a/lib/php/test/Unit/Lib/Serializer/TBinarySerializerTest.php +++ b/lib/php/test/Unit/Lib/Serializer/TBinarySerializerTest.php @@ -22,17 +22,135 @@ namespace Test\Thrift\Unit\Lib\Serializer; +use phpmock\phpunit\PHPMock; use PHPUnit\Framework\TestCase; +use Test\Thrift\Unit\Lib\Fixture\TestSerializerStruct; +use Thrift\Serializer\TBinarySerializer; class TBinarySerializerTest extends TestCase { - public function testSerialize() + use PHPMock; + + /** + * @dataProvider serializeDeserializeDataProvider + */ + public function testSerializeAndDeserialize($stringVal, $intVal) + { + $object = new TestSerializerStruct(); + $object->stringField = $stringVal; + $object->intField = $intVal; + + $serialized = TBinarySerializer::serialize($object); + + $this->assertNotEmpty($serialized); + $this->assertIsString($serialized); + + $deserialized = TBinarySerializer::deserialize($serialized, TestSerializerStruct::class); + + $this->assertInstanceOf(TestSerializerStruct::class, $deserialized); + $this->assertEquals($stringVal, $deserialized->stringField); + $this->assertEquals($intVal, $deserialized->intField); + } + + public function serializeDeserializeDataProvider() + { + yield 'both fields' => [ + 'stringVal' => 'hello', + 'intVal' => 42, + ]; + yield 'empty struct' => [ + 'stringVal' => null, + 'intVal' => null, + ]; + yield 'only string field' => [ + 'stringVal' => 'test value', + 'intVal' => null, + ]; + yield 'only int field' => [ + 'stringVal' => null, + 'intVal' => 12345, + ]; + yield 'empty string' => [ + 'stringVal' => '', + 'intVal' => 0, + ]; + yield 'special characters' => [ + 'stringVal' => "line1\nline2\ttab \"quotes\"", + 'intVal' => -1, + ]; + yield 'large int' => [ + 'stringVal' => 'max', + 'intVal' => 2147483647, + ]; + yield 'negative int' => [ + 'stringVal' => 'min', + 'intVal' => -2147483648, + ]; + } + + public function testDeserializeWithCustomBufferSize() + { + $object = new TestSerializerStruct(); + $object->stringField = 'buffer test'; + $object->intField = 99; + + $serialized = TBinarySerializer::serialize($object); + $deserialized = TBinarySerializer::deserialize($serialized, TestSerializerStruct::class, 1024); + + $this->assertInstanceOf(TestSerializerStruct::class, $deserialized); + $this->assertEquals('buffer test', $deserialized->stringField); + $this->assertEquals(99, $deserialized->intField); + } + + public function testSerializeWithAcceleratedExtension() { - $this->markTestIncomplete('Could not test static function which create instances during execution'); + $object = new TestSerializerStruct(); + $object->stringField = 'accel'; + $object->intField = 7; + + $funcExists = $this->getFunctionMock('Thrift\Serializer', 'function_exists'); + $funcExists->expects($this->atLeastOnce()) + ->willReturn(true); + + $writeFunc = $this->getFunctionMock('Thrift\Serializer', 'thrift_protocol_write_binary'); + $writeFunc->expects($this->once()) + ->willReturnCallback(function ($protocol, $name, $type, $object, $seqid, $strictWrite) { + // Simulate C extension: write message header + struct + $protocol->writeMessageBegin($name, $type, $seqid); + $object->write($protocol); + $protocol->writeMessageEnd(); + }); + + $serialized = TBinarySerializer::serialize($object); + + $this->assertNotEmpty($serialized); + $this->assertIsString($serialized); } - public function testDeserialize() + public function testDeserializeWithAcceleratedExtension() { - $this->markTestIncomplete('Could not test static function which create instances during execution'); + $object = new TestSerializerStruct(); + $object->stringField = 'accel'; + $object->intField = 7; + + $serialized = TBinarySerializer::serialize($object); + + $funcExists = $this->getFunctionMock('Thrift\Serializer', 'function_exists'); + $funcExists->expects($this->atLeastOnce()) + ->willReturn(true); + + $expectedResult = new TestSerializerStruct(); + $expectedResult->stringField = 'accel'; + $expectedResult->intField = 7; + + $readFunc = $this->getFunctionMock('Thrift\Serializer', 'thrift_protocol_read_binary'); + $readFunc->expects($this->once()) + ->willReturn($expectedResult); + + $deserialized = TBinarySerializer::deserialize($serialized, TestSerializerStruct::class); + + $this->assertInstanceOf(TestSerializerStruct::class, $deserialized); + $this->assertEquals('accel', $deserialized->stringField); + $this->assertEquals(7, $deserialized->intField); } } diff --git a/lib/php/test/Unit/Lib/Server/TForkingServerTest.php b/lib/php/test/Unit/Lib/Server/TForkingServerTest.php index 0a440329f49..717753bad8c 100644 --- a/lib/php/test/Unit/Lib/Server/TForkingServerTest.php +++ b/lib/php/test/Unit/Lib/Server/TForkingServerTest.php @@ -21,12 +21,255 @@ namespace Test\Thrift\Unit\Lib\Server; +use phpmock\phpunit\PHPMock; use PHPUnit\Framework\TestCase; +use Test\Thrift\Unit\Lib\ReflectionHelper; +use Thrift\Exception\TException; +use Thrift\Exception\TTransportException; +use Thrift\Factory\TProtocolFactory; +use Thrift\Factory\TTransportFactoryInterface; +use Thrift\Server\TForkingServer; +use Thrift\Server\TServerTransport; +use Thrift\Transport\TTransport; class TForkingServerTest extends TestCase { - public function testServe(): void + use PHPMock; + use ReflectionHelper; + + private function createServer( + $processor = null, + $transport = null, + $inputTransportFactory = null, + $outputTransportFactory = null, + $inputProtocolFactory = null, + $outputProtocolFactory = null + ): TForkingServer { + return new TForkingServer( + $processor ?? new \stdClass(), + $transport ?? $this->createMock(TServerTransport::class), + $inputTransportFactory ?? $this->createMock(TTransportFactoryInterface::class), + $outputTransportFactory ?? $this->createMock(TTransportFactoryInterface::class), + $inputProtocolFactory ?? $this->createMock(TProtocolFactory::class), + $outputProtocolFactory ?? $this->createMock(TProtocolFactory::class) + ); + } + + public function testStopClosesTransportAndSetsFlag() + { + $transport = $this->createMock(TServerTransport::class); + $transport->expects($this->once())->method('close'); + + $server = $this->createServer(null, $transport); + $server->stop(); + + $this->assertTrue($this->getPropertyValue($server, 'stop_')); + } + + public function testChildrenArrayInitiallyEmpty() + { + $server = $this->createServer(); + $this->assertEmpty($this->getPropertyValue($server, 'children_')); + } + + public function testConstructorStoresCollaborators() + { + $processor = new \stdClass(); + $transport = $this->createMock(TServerTransport::class); + $inputTransportFactory = $this->createMock(TTransportFactoryInterface::class); + $outputTransportFactory = $this->createMock(TTransportFactoryInterface::class); + $inputProtocolFactory = $this->createMock(TProtocolFactory::class); + $outputProtocolFactory = $this->createMock(TProtocolFactory::class); + + $server = $this->createServer( + $processor, + $transport, + $inputTransportFactory, + $outputTransportFactory, + $inputProtocolFactory, + $outputProtocolFactory + ); + + $this->assertSame($processor, $this->getPropertyValue($server, 'processor_')); + $this->assertSame($transport, $this->getPropertyValue($server, 'transport_')); + $this->assertSame($inputTransportFactory, $this->getPropertyValue($server, 'inputTransportFactory_')); + $this->assertSame($outputTransportFactory, $this->getPropertyValue($server, 'outputTransportFactory_')); + $this->assertSame($inputProtocolFactory, $this->getPropertyValue($server, 'inputProtocolFactory_')); + $this->assertSame($outputProtocolFactory, $this->getPropertyValue($server, 'outputProtocolFactory_')); + } + + public function testServeListensAndLoopsUntilStopped() + { + $serverTransport = $this->createMock(TServerTransport::class); + $serverTransport->expects($this->once())->method('listen'); + + $server = $this->createServer(null, $serverTransport); + + // accept returns null (no connection), then on second call we stop + $callCount = 0; + $serverTransport->method('accept')->willReturnCallback( + function () use ($server, &$callCount) { + $callCount++; + if ($callCount >= 2) { + $this->setPropertyValue($server, 'stop_', true); + } + return null; + } + ); + + $this->getFunctionMock('Thrift\Server', 'pcntl_waitpid') + ->expects($this->any()) + ->willReturn(0); + + $server->serve(); + + $this->assertGreaterThanOrEqual(2, $callCount); + } + + public function testServeForksAndHandlesParent() + { + $serverTransport = $this->createMock(TServerTransport::class); + $serverTransport->expects($this->once())->method('listen'); + + $clientTransport = $this->createMock(TTransport::class); + $server = $this->createServer(null, $serverTransport); + + $callCount = 0; + $serverTransport->method('accept')->willReturnCallback( + function () use ($server, $clientTransport, &$callCount) { + $callCount++; + if ($callCount === 1) { + return $clientTransport; + } + $this->setPropertyValue($server, 'stop_', true); + return null; + } + ); + + $this->getFunctionMock('Thrift\Server', 'pcntl_fork') + ->expects($this->once()) + ->willReturn(12345); + + $this->getFunctionMock('Thrift\Server', 'pcntl_waitpid') + ->expects($this->any()) + ->willReturn(0); + + $server->serve(); + + $children = $this->getPropertyValue($server, 'children_'); + $this->assertArrayHasKey(12345, $children); + $this->assertSame($clientTransport, $children[12345]); + } + + public function testServeThrowsTExceptionOnForkFailure() + { + $this->expectException(TException::class); + $this->expectExceptionMessage('Failed to fork'); + + $serverTransport = $this->createMock(TServerTransport::class); + $clientTransport = $this->createMock(TTransport::class); + + $server = $this->createServer(null, $serverTransport); + + $serverTransport->method('accept')->willReturn($clientTransport); + + $this->getFunctionMock('Thrift\Server', 'pcntl_fork') + ->expects($this->once()) + ->willReturn(-1); + + $this->getFunctionMock('Thrift\Server', 'pcntl_waitpid') + ->expects($this->any()) + ->willReturn(0); + + $server->serve(); + } + + public function testServeIgnoresTTransportException() + { + $serverTransport = $this->createMock(TServerTransport::class); + $serverTransport->expects($this->once())->method('listen'); + + $server = $this->createServer(null, $serverTransport); + + $callCount = 0; + $serverTransport->method('accept')->willReturnCallback( + function () use ($server, &$callCount) { + $callCount++; + if ($callCount === 1) { + throw new TTransportException('Connection reset'); + } + $this->setPropertyValue($server, 'stop_', true); + return null; + } + ); + + $this->getFunctionMock('Thrift\Server', 'pcntl_waitpid') + ->expects($this->any()) + ->willReturn(0); + + $server->serve(); + + $this->assertGreaterThanOrEqual(2, $callCount); + } + + public function testCollectChildrenRemovesFinishedAndClosesTransport() + { + $server = $this->createServer(); + + $transport1 = $this->createMock(TTransport::class); + $transport1->expects($this->once())->method('close'); + + $transport2 = $this->createMock(TTransport::class); + $transport2->expects($this->never())->method('close'); + + $this->setPropertyValue($server, 'children_', [ + 111 => $transport1, + 222 => $transport2, + ]); + + $this->getFunctionMock('Thrift\Server', 'pcntl_waitpid') + ->expects($this->exactly(2)) + ->willReturnCallback(function ($pid) { + return ($pid === 111) ? 111 : 0; + }); + + $method = $this->getAccessibleMethod($server, 'collectChildren'); + $method->invoke($server); + + $children = $this->getPropertyValue($server, 'children_'); + $this->assertArrayNotHasKey(111, $children); + $this->assertArrayHasKey(222, $children); + } + + public function testCollectChildrenHandlesNullTransport() { - $this->markTestSkipped('Unit test could not be written for class which use pcntl_fork and exit functions'); + $server = $this->createServer(); + + $this->setPropertyValue($server, 'children_', [ + 333 => null, + ]); + + $this->getFunctionMock('Thrift\Server', 'pcntl_waitpid') + ->expects($this->once()) + ->willReturn(333); + + $method = $this->getAccessibleMethod($server, 'collectChildren'); + $method->invoke($server); + + $children = $this->getPropertyValue($server, 'children_'); + $this->assertEmpty($children); + } + + public function testHandleParentStoresChildPid() + { + $server = $this->createServer(); + $transport = $this->createMock(TTransport::class); + + $method = $this->getAccessibleMethod($server, 'handleParent'); + $method->invoke($server, $transport, 42); + + $children = $this->getPropertyValue($server, 'children_'); + $this->assertArrayHasKey(42, $children); + $this->assertSame($transport, $children[42]); } } diff --git a/lib/php/test/Unit/Lib/TMultiplexedProcessorTest.php b/lib/php/test/Unit/Lib/TMultiplexedProcessorTest.php index 83bebe50d70..365259e5941 100644 --- a/lib/php/test/Unit/Lib/TMultiplexedProcessorTest.php +++ b/lib/php/test/Unit/Lib/TMultiplexedProcessorTest.php @@ -22,7 +22,7 @@ namespace Test\Thrift\Unit\Lib; use PHPUnit\Framework\TestCase; -use Test\Thrift\Unit\Lib\Fixture\ProcessorSpy; +use Test\Thrift\Unit\Lib\Fixture\TestProcessor; use Thrift\Exception\TException; use Thrift\Protocol\TProtocol; use Thrift\StoredMessageProtocol; @@ -37,7 +37,7 @@ public function testProcessDispatchesToRegisteredService(): void $transport = $this->createMock(TTransport::class); $input = $this->createMock(TProtocol::class); $output = $this->createMock(TProtocol::class); - $processor = $this->createMock(ProcessorSpy::class); + $processor = $this->createMock(TestProcessor::class); $input->expects($this->once()) ->method('readMessageBegin') @@ -136,7 +136,7 @@ public function testProcessRequiresKnownServiceName(): void }); $multiplexedProcessor = new TMultiplexedProcessor(); - $multiplexedProcessor->registerProcessor('Other', $this->createMock(ProcessorSpy::class)); + $multiplexedProcessor->registerProcessor('Other', $this->createMock(TestProcessor::class)); $this->expectException(TException::class); $this->expectExceptionMessage('Service name not found: Missing.'); diff --git a/lib/php/test/Unit/Lib/Transport/TransportErrorScenariosTest.php b/lib/php/test/Unit/Lib/Transport/TransportErrorScenariosTest.php new file mode 100644 index 00000000000..054d9174f01 --- /dev/null +++ b/lib/php/test/Unit/Lib/Transport/TransportErrorScenariosTest.php @@ -0,0 +1,196 @@ +expectException(TTransportException::class); + $this->expectExceptionCode(TTransportException::UNKNOWN); + + $buffer->read(10); + } + + public function testMemoryBufferWriteEmptyStringAvailableReturnsZero() + { + $buffer = new TMemoryBuffer(); + $buffer->write(''); + + $this->assertEquals(0, $buffer->available()); + } + + public function testMemoryBufferReadExactBufferSizeWorks() + { + $data = 'hello'; + $buffer = new TMemoryBuffer($data); + + $result = $buffer->read(5); + + $this->assertEquals('hello', $result); + $this->assertEquals(0, $buffer->available()); + } + + public function testMemoryBufferReadAllWithInsufficientDataThrowsException() + { + $buffer = new TMemoryBuffer('abc'); + + $this->expectException(TTransportException::class); + + // readAll needs 10 bytes but only 3 are available; + // after reading 3 bytes the buffer is empty and the next read() throws + $buffer->readAll(10); + } + + public function testMemoryBufferGetBufferReturnsWhatWasWritten() + { + $buffer = new TMemoryBuffer(); + $buffer->write('first'); + $buffer->write('second'); + + $this->assertEquals('firstsecond', $buffer->getBuffer()); + } + + // ========================================================================= + // TBufferedTransport error cases + // ========================================================================= + + public function testBufferedTransportReadAfterCloseThrowsException() + { + $transport = $this->createMock(TTransport::class); + $bufferedTransport = new TBufferedTransport($transport); + + $transport + ->expects($this->once()) + ->method('close'); + + $transport + ->method('read') + ->willThrowException( + new TTransportException('Transport not open', TTransportException::NOT_OPEN) + ); + + $bufferedTransport->close(); + + $this->expectException(TTransportException::class); + $this->expectExceptionCode(TTransportException::NOT_OPEN); + + $bufferedTransport->read(10); + } + + public function testBufferedTransportWriteOnUnderlyingTransportErrorThrowsException() + { + $transport = $this->createMock(TTransport::class); + // Use a small write buffer so that write triggers a flush to underlying transport + $bufferedTransport = new TBufferedTransport($transport, 512, 10); + + $transport + ->method('write') + ->willThrowException( + new TTransportException('Write failed', TTransportException::UNKNOWN) + ); + + $this->expectException(TTransportException::class); + + // Write more than wBufSize_ to trigger underlying write + $bufferedTransport->write('this string is longer than ten bytes'); + } + + public function testBufferedTransportFlushOnUnderlyingTransportErrorThrowsException() + { + $transport = $this->createMock(TTransport::class); + $bufferedTransport = new TBufferedTransport($transport); + + $transport + ->method('write') + ->willThrowException( + new TTransportException('Flush write failed', TTransportException::UNKNOWN) + ); + + // Write some data to the buffer first + $bufferedTransport->write('data'); + + $this->expectException(TTransportException::class); + + $bufferedTransport->flush(); + } + + // ========================================================================= + // TFramedTransport error cases + // ========================================================================= + + public function testFramedTransportReadWithNoFrameAvailableThrowsException() + { + $transport = $this->createMock(TTransport::class); + $framedTransport = new TFramedTransport($transport); + + // readFrame calls transport_->readAll(4) which will throw when no data available + $transport + ->method('readAll') + ->willThrowException( + new TTransportException( + 'TMemoryBuffer: Could not read 4 bytes from buffer.', + TTransportException::UNKNOWN + ) + ); + + $this->expectException(TTransportException::class); + + $framedTransport->read(10); + } + + public function testFramedTransportReadFrameOnClosedUnderlyingTransportThrowsException() + { + $transport = $this->createMock(TTransport::class); + $framedTransport = new TFramedTransport($transport); + + $transport + ->expects($this->once()) + ->method('close'); + + $transport + ->method('readAll') + ->willThrowException( + new TTransportException('Transport not open', TTransportException::NOT_OPEN) + ); + + $framedTransport->close(); + + $this->expectException(TTransportException::class); + $this->expectExceptionCode(TTransportException::NOT_OPEN); + + $framedTransport->read(10); + } +}