diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3369f83248d..d889e805e1c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -188,11 +188,13 @@ jobs: - name: Build Thrift Classes run: | mkdir -p ./lib/php/test/Resources/packages/php + mkdir -p ./lib/php/test/Resources/packages/phpi mkdir -p ./lib/php/test/Resources/packages/phpv mkdir -p ./lib/php/test/Resources/packages/phpvo mkdir -p ./lib/php/test/Resources/packages/phpjs mkdir -p ./lib/php/test/Resources/packages/phpcm compiler/cpp/thrift --gen php:nsglobal="Basic" -r --out ./lib/php/test/Resources/packages/php lib/php/test/Resources/ThriftTest.thrift + compiler/cpp/thrift --gen php:inlined,nsglobal="BasicInline" -r --out ./lib/php/test/Resources/packages/phpi lib/php/test/Resources/ThriftTest.thrift compiler/cpp/thrift --gen php:validate,nsglobal="Validate" -r --out ./lib/php/test/Resources/packages/phpv lib/php/test/Resources/ThriftTest.thrift compiler/cpp/thrift --gen php:validate,oop,nsglobal="ValidateOop" -r --out ./lib/php/test/Resources/packages/phpvo lib/php/test/Resources/ThriftTest.thrift compiler/cpp/thrift --gen php:json,nsglobal="Json" -r --out ./lib/php/test/Resources/packages/phpjs lib/php/test/Resources/ThriftTest.thrift diff --git a/lib/php/lib/Protocol/TProtocol.php b/lib/php/lib/Protocol/TProtocol.php index 400078a82d4..a486f0b691a 100644 --- a/lib/php/lib/Protocol/TProtocol.php +++ b/lib/php/lib/Protocol/TProtocol.php @@ -266,19 +266,33 @@ public static function skipBinary($itrans, $type) { switch ($type) { case TType::BOOL: - return $itrans->readAll(1); + $itrans->readAll(1); + + return 1; case TType::BYTE: - return $itrans->readAll(1); + $itrans->readAll(1); + + return 1; case TType::I16: - return $itrans->readAll(2); + $itrans->readAll(2); + + return 2; case TType::I32: - return $itrans->readAll(4); + $itrans->readAll(4); + + return 4; case TType::I64: - return $itrans->readAll(8); + $itrans->readAll(8); + + return 8; case TType::DOUBLE: - return $itrans->readAll(8); + $itrans->readAll(8); + + return 8; case TType::UUID: - return $itrans->readAll(16); + $itrans->readAll(16); + + return 16; case TType::STRING: $len = unpack('N', $itrans->readAll(4)); $len = $len[1]; @@ -286,7 +300,9 @@ public static function skipBinary($itrans, $type) $len = 0 - (($len - 1) ^ 0xffffffff); } - return 4 + $itrans->readAll($len); + $itrans->readAll($len); + + return 4 + $len; case TType::STRUCT: $result = 0; @@ -294,13 +310,15 @@ public static function skipBinary($itrans, $type) $ftype = 0; $fid = 0; $data = $itrans->readAll(1); + $result += 1; $arr = unpack('c', $data); $ftype = $arr[1]; if ($ftype == TType::STOP) { break; } // I16 field id - $result += $itrans->readAll(2); + $itrans->readAll(2); + $result += 2; $result += self::skipBinary($itrans, $ftype); } diff --git a/lib/php/test/Integration/Lib/Protocol/TProtocolInlinedTest.php b/lib/php/test/Integration/Lib/Protocol/TProtocolInlinedTest.php new file mode 100644 index 00000000000..f0e1958fcb2 --- /dev/null +++ b/lib/php/test/Integration/Lib/Protocol/TProtocolInlinedTest.php @@ -0,0 +1,77 @@ + 1, + 'newint' => 2, + 'newbyte' => 3, + 'newshort' => 4, + 'newlong' => 5, + 'newdouble' => 6.25, + 'newstruct' => new Bonk([ + 'message' => 'skip me', + 'type' => 7, + ]), + 'newlist' => [8, 9], + 'newset' => [10, 11], + 'newmap' => [12 => 13], + 'newstring' => 'skip me too', + 'end_in_both' => 14, + ]); + + $buffer = ''; + $writer->write($buffer); + + $transport = new TMemoryBuffer($buffer); + $reader = new VersioningTestV1(); + $reader->read($transport); + + $this->assertSame(1, $reader->begin_in_both); + $this->assertNull($reader->old_string); + $this->assertSame(14, $reader->end_in_both); + $this->assertSame(0, (int)$transport->available()); + } +} diff --git a/lib/php/test/Makefile.am b/lib/php/test/Makefile.am index b6a53256e2b..674b07160cb 100644 --- a/lib/php/test/Makefile.am +++ b/lib/php/test/Makefile.am @@ -21,11 +21,13 @@ PHPUNIT=php $(top_srcdir)/vendor/bin/phpunit stubs: Resources/ThriftTest.thrift mkdir -p ./Resources/packages/php + mkdir -p ./Resources/packages/phpi mkdir -p ./Resources/packages/phpv mkdir -p ./Resources/packages/phpvo mkdir -p ./Resources/packages/phpjs mkdir -p ./Resources/packages/phpcm $(THRIFT) --gen php:nsglobal="Basic" -r --out ./Resources/packages/php Resources/ThriftTest.thrift + $(THRIFT) --gen php:inlined,nsglobal="BasicInline" -r --out ./Resources/packages/phpi Resources/ThriftTest.thrift $(THRIFT) --gen php:validate,nsglobal="Validate" -r --out ./Resources/packages/phpv Resources/ThriftTest.thrift $(THRIFT) --gen php:validate,oop,nsglobal="ValidateOop" -r --out ./Resources/packages/phpvo Resources/ThriftTest.thrift $(THRIFT) --gen php:json,nsglobal="Json" -r --out ./Resources/packages/phpjs Resources/ThriftTest.thrift diff --git a/lib/php/test/Unit/Lib/Base/Fixture/ComplexStruct.php b/lib/php/test/Unit/Lib/Base/Fixture/ComplexStruct.php new file mode 100644 index 00000000000..124fae017b7 --- /dev/null +++ b/lib/php/test/Unit/Lib/Base/Fixture/ComplexStruct.php @@ -0,0 +1,112 @@ + [ + 'var' => 'flag', + 'type' => TType::BOOL, + ], + 2 => [ + 'var' => 'name', + 'type' => TType::STRING, + ], + 3 => [ + 'var' => 'child', + 'type' => TType::STRUCT, + 'class' => NestedStruct::class, + ], + 4 => [ + 'var' => 'mapField', + 'type' => TType::MAP, + 'ktype' => TType::STRING, + 'vtype' => TType::I32, + 'key' => [ + 'type' => TType::STRING, + ], + 'val' => [ + 'type' => TType::I32, + ], + ], + 5 => [ + 'var' => 'listField', + 'type' => TType::LST, + 'etype' => TType::STRUCT, + 'elem' => [ + 'type' => TType::STRUCT, + 'class' => NestedStruct::class, + ], + ], + 6 => [ + 'var' => 'setField', + 'type' => TType::SET, + 'etype' => TType::I16, + 'elem' => [ + 'type' => TType::I16, + ], + ], + 7 => [ + 'var' => 'mapOfLists', + 'type' => TType::MAP, + 'ktype' => TType::I32, + 'vtype' => TType::LST, + 'key' => [ + 'type' => TType::I32, + ], + 'val' => [ + 'type' => TType::LST, + 'etype' => TType::I32, + 'elem' => [ + 'type' => TType::I32, + ], + ], + ], + 8 => [ + 'var' => 'optionalField', + 'type' => TType::STRING, + ], + ]; + + public $flag = null; + public $name = null; + public $child = null; + public $mapField = null; + public $listField = null; + public $setField = null; + public $mapOfLists = null; + public $optionalField = null; + + public function read($input) + { + return $this->_read(self::class, self::$_TSPEC, $input); + } + + public function write($output) + { + return $this->_write('ComplexStruct', self::$_TSPEC, $output); + } +} diff --git a/lib/php/test/Unit/Lib/Base/Fixture/NestedStruct.php b/lib/php/test/Unit/Lib/Base/Fixture/NestedStruct.php new file mode 100644 index 00000000000..742581368f8 --- /dev/null +++ b/lib/php/test/Unit/Lib/Base/Fixture/NestedStruct.php @@ -0,0 +1,47 @@ + [ + 'var' => 'value', + 'type' => TType::STRING, + ], + ]; + + public $value = null; + + public function read($input) + { + return $this->_read(self::class, self::$_TSPEC, $input); + } + + public function write($output) + { + return $this->_write('NestedStruct', self::$_TSPEC, $output); + } +} diff --git a/lib/php/test/Unit/Lib/Base/TBaseTest.php b/lib/php/test/Unit/Lib/Base/TBaseTest.php new file mode 100644 index 00000000000..3808aaae9a5 --- /dev/null +++ b/lib/php/test/Unit/Lib/Base/TBaseTest.php @@ -0,0 +1,136 @@ + true, + 'name' => 'hydrated', + ] + ); + + $this->assertTrue($struct->flag); + $this->assertSame('hydrated', $struct->name); + $this->assertNull($struct->child); + } + + public function testWakeupPreservesExistingState(): void + { + $struct = $this->createComplexStruct(); + + /** @var ComplexStruct $restored */ + $restored = unserialize(serialize($struct)); + + $this->assertEquals($struct, $restored); + } + + public function testReadAndWriteRoundTripNestedContainers(): void + { + $restored = $this->roundTrip($this->createComplexStruct()); + + $this->assertTrue($restored->flag); + $this->assertSame('root', $restored->name); + $this->assertInstanceOf(NestedStruct::class, $restored->child); + $this->assertSame('child', $restored->child->value); + $this->assertSame(['alpha' => 1, 'beta' => 2], $restored->mapField); + $this->assertCount(2, $restored->listField); + $this->assertSame('first', $restored->listField[0]->value); + $this->assertSame('second', $restored->listField[1]->value); + $this->assertSame([10 => true, 20 => true], $restored->setField); + $this->assertSame([1 => [3, 4], 2 => [5]], $restored->mapOfLists); + $this->assertNull($restored->optionalField); + } + + public function testReadSkipsUnknownAndUnexpectedFields(): void + { + $transport = new TMemoryBuffer(); + $protocol = new TBinaryProtocol($transport); + + $protocol->writeStructBegin('ComplexStruct'); + + $protocol->writeFieldBegin('flag', TType::STRING, 1); + $protocol->writeString('ignored'); + $protocol->writeFieldEnd(); + + $protocol->writeFieldBegin('unknown', TType::I32, 99); + $protocol->writeI32(123); + $protocol->writeFieldEnd(); + + $protocol->writeFieldBegin('name', TType::STRING, 2); + $protocol->writeString('kept'); + $protocol->writeFieldEnd(); + + $protocol->writeFieldStop(); + $protocol->writeStructEnd(); + + $struct = new ComplexStruct(); + $struct->read(new TBinaryProtocol($transport)); + + $this->assertNull($struct->flag); + $this->assertSame('kept', $struct->name); + $this->assertNull($struct->child); + } + + private function roundTrip(ComplexStruct $struct): ComplexStruct + { + $transport = new TMemoryBuffer(); + $writer = new TBinaryProtocol($transport); + $struct->write($writer); + + $copy = new ComplexStruct(); + $copy->read(new TBinaryProtocol($transport)); + + return $copy; + } + + private function createComplexStruct(): ComplexStruct + { + $child = new NestedStruct(NestedStruct::$_TSPEC, ['value' => 'child']); + $first = new NestedStruct(NestedStruct::$_TSPEC, ['value' => 'first']); + $second = new NestedStruct(NestedStruct::$_TSPEC, ['value' => 'second']); + + return new ComplexStruct( + ComplexStruct::$_TSPEC, + [ + 'flag' => true, + 'name' => 'root', + 'child' => $child, + 'mapField' => ['alpha' => 1, 'beta' => 2], + 'listField' => [$first, $second], + 'setField' => [10 => true, 20 => true], + 'mapOfLists' => [1 => [3, 4], 2 => [5]], + ] + ); + } +} diff --git a/lib/php/test/Unit/Lib/Exception/ExceptionTypesTest.php b/lib/php/test/Unit/Lib/Exception/ExceptionTypesTest.php new file mode 100644 index 00000000000..249656299e0 --- /dev/null +++ b/lib/php/test/Unit/Lib/Exception/ExceptionTypesTest.php @@ -0,0 +1,51 @@ +assertSame('invalid payload', $exception->getMessage()); + $this->assertSame(TProtocolException::BAD_VERSION, $exception->getCode()); + } + + public function testTransportExceptionPreservesMessageAndCode(): void + { + $exception = new TTransportException( + 'timed out', + TTransportException::TIMED_OUT + ); + + $this->assertSame('timed out', $exception->getMessage()); + $this->assertSame(TTransportException::TIMED_OUT, $exception->getCode()); + } +} diff --git a/lib/php/test/Unit/Lib/Exception/TApplicationExceptionTest.php b/lib/php/test/Unit/Lib/Exception/TApplicationExceptionTest.php new file mode 100644 index 00000000000..b43cf14f227 --- /dev/null +++ b/lib/php/test/Unit/Lib/Exception/TApplicationExceptionTest.php @@ -0,0 +1,59 @@ +write($writer); + + $read = new TApplicationException(); + $read->read(new TBinaryProtocol($transport)); + + $this->assertSame('boom', $read->getMessage()); + $this->assertSame(TApplicationException::INTERNAL_ERROR, $read->getCode()); + } + + public function testWriteOmitsFalsyMessageAndCodeFields(): void + { + $transport = new TMemoryBuffer(); + $protocol = new TBinaryProtocol($transport); + + (new TApplicationException())->write($protocol); + + $this->assertSame(chr(TType::STOP), $transport->getBuffer()); + } +} diff --git a/lib/php/test/Unit/Lib/Fixture/ProcessorSpy.php b/lib/php/test/Unit/Lib/Fixture/ProcessorSpy.php new file mode 100644 index 00000000000..c0005dcca5c --- /dev/null +++ b/lib/php/test/Unit/Lib/Fixture/ProcessorSpy.php @@ -0,0 +1,31 @@ +transport = $transport; + } + + public function getTransport(): TTransport + { + return $this->transport; + } + + public function readJSONSyntaxChar(string $char): void + { + $this->readChars[] = $char; + } +} diff --git a/lib/php/test/Unit/Lib/Protocol/Fixture/SimpleJsonProtocolStub.php b/lib/php/test/Unit/Lib/Protocol/Fixture/SimpleJsonProtocolStub.php new file mode 100644 index 00000000000..e61795ea020 --- /dev/null +++ b/lib/php/test/Unit/Lib/Protocol/Fixture/SimpleJsonProtocolStub.php @@ -0,0 +1,39 @@ +transport = $transport; + } + + public function getTransport(): TMemoryBuffer + { + return $this->transport; + } +} diff --git a/lib/php/test/Unit/Lib/Protocol/JsonContextTest.php b/lib/php/test/Unit/Lib/Protocol/JsonContextTest.php new file mode 100644 index 00000000000..a7f569e5c8e --- /dev/null +++ b/lib/php/test/Unit/Lib/Protocol/JsonContextTest.php @@ -0,0 +1,116 @@ +assertFalse($context->escapeNum()); + $this->assertNull($context->write()); + $this->assertNull($context->read()); + } + + public function testListContextWritesCommasAfterFirstElement(): void + { + $transport = new TMemoryBuffer(); + $context = new ListContext(new JsonProtocolStub($transport)); + + $context->write(); + $context->write(); + $context->write(); + + $this->assertSame(',,', $transport->getBuffer()); + } + + public function testListContextReadsCommasAfterFirstElement(): void + { + $protocol = new JsonProtocolStub(new TMemoryBuffer()); + $context = new ListContext($protocol); + + $context->read(); + $context->read(); + $context->read(); + + $this->assertCount(2, $protocol->readChars); + $this->assertSame([',', ','], $protocol->readChars); + } + + public function testPairContextWritesColonsAndCommas(): void + { + $transport = new TMemoryBuffer(); + $context = new PairContext(new JsonProtocolStub($transport)); + + $this->assertTrue($context->escapeNum()); + + $context->write(); + $this->assertTrue($context->escapeNum()); + + $context->write(); + $this->assertFalse($context->escapeNum()); + + $context->write(); + $this->assertTrue($context->escapeNum()); + + $this->assertSame(':,', $transport->getBuffer()); + } + + public function testPairContextReadsColonsAndCommas(): void + { + $protocol = new JsonProtocolStub(new TMemoryBuffer()); + $context = new PairContext($protocol); + + $context->read(); + $context->read(); + $context->read(); + + $this->assertCount(2, $protocol->readChars); + $this->assertSame([':', ','], $protocol->readChars); + } + + public function testLookaheadReaderCachesPeekedValue(): void + { + $transport = $this->createMock(TTransport::class); + $transport->expects($this->exactly(2)) + ->method('readAll') + ->with(1) + ->willReturnOnConsecutiveCalls('a', 'b'); + + $reader = new LookaheadReader(new JsonProtocolStub($transport)); + + $this->assertSame('a', $reader->peek()); + $this->assertSame('a', $reader->peek()); + $this->assertSame('a', $reader->read()); + $this->assertSame('b', $reader->read()); + } +} diff --git a/lib/php/test/Unit/Lib/Protocol/SimpleJsonContextTest.php b/lib/php/test/Unit/Lib/Protocol/SimpleJsonContextTest.php new file mode 100644 index 00000000000..b80c66e61e8 --- /dev/null +++ b/lib/php/test/Unit/Lib/Protocol/SimpleJsonContextTest.php @@ -0,0 +1,94 @@ +assertNull($context->write()); + $this->assertFalse($context->isMapKey()); + } + + public function testListContextWritesCommasAfterFirstElement(): void + { + $transport = new TMemoryBuffer(); + $context = new ListContext(new SimpleJsonProtocolStub($transport)); + + $context->write(); + $context->write(); + $context->write(); + + $this->assertSame(',,', $transport->getBuffer()); + } + + public function testStructContextWritesColonsAndCommas(): void + { + $transport = new TMemoryBuffer(); + $context = new StructContext(new SimpleJsonProtocolStub($transport)); + + $context->write(); + $context->write(); + $context->write(); + + $this->assertSame(':,', $transport->getBuffer()); + } + + public function testMapContextTogglesMapKeyState(): void + { + $transport = new TMemoryBuffer(); + $context = new MapContext(new SimpleJsonProtocolStub($transport)); + + $this->assertTrue($context->isMapKey()); + + $context->write(); + $this->assertFalse($context->isMapKey()); + + $context->write(); + $this->assertTrue($context->isMapKey()); + + $context->write(); + $this->assertFalse($context->isMapKey()); + + $this->assertSame(':,', $transport->getBuffer()); + } + + public function testCollectionMapKeyExceptionPreservesMessage(): void + { + $exception = new CollectionMapKeyException('bad key'); + + $this->assertInstanceOf(TException::class, $exception); + $this->assertSame('bad key', $exception->getMessage()); + } +} diff --git a/lib/php/test/Unit/Lib/Protocol/TProtocolTest.php b/lib/php/test/Unit/Lib/Protocol/TProtocolTest.php new file mode 100644 index 00000000000..b75c09e87f7 --- /dev/null +++ b/lib/php/test/Unit/Lib/Protocol/TProtocolTest.php @@ -0,0 +1,284 @@ +buildBinaryBuffer( + function (TBinaryProtocol $protocol) use ($writerMethod, $value): void { + $protocol->{$writerMethod}($value); + } + ); + $transport = new TMemoryBuffer($buffer); + $protocol = new TBinaryProtocol($transport); + + $this->assertSame(strlen($buffer), $protocol->skip($type)); + $this->assertSame(0, (int)$transport->available()); + } + + public function skipScalarDataProvider(): iterable + { + yield 'bool' => [ + TType::BOOL, + 'writeBool', + true, + ]; + yield 'byte' => [ + TType::BYTE, + 'writeByte', + 7, + ]; + yield 'i16' => [ + TType::I16, + 'writeI16', + 1024, + ]; + yield 'i32' => [ + TType::I32, + 'writeI32', + 65536, + ]; + yield 'i64' => [ + TType::I64, + 'writeI64', + 1099511627776, + ]; + yield 'double' => [ + TType::DOUBLE, + 'writeDouble', + 3.14, + ]; + yield 'string' => [ + TType::STRING, + 'writeString', + 'skip me', + ]; + yield 'uuid' => [ + TType::UUID, + 'writeUuid', + '12345678-1234-5678-1234-567812345678', + ]; + } + + public function testSkipStruct(): void + { + $buffer = $this->buildBinaryBuffer( + function (TBinaryProtocol $protocol): void { + $protocol->writeStructBegin('Example'); + $protocol->writeFieldBegin('flag', TType::BOOL, 1); + $protocol->writeBool(true); + $protocol->writeFieldEnd(); + $protocol->writeFieldBegin('name', TType::STRING, 2); + $protocol->writeString('value'); + $protocol->writeFieldEnd(); + $protocol->writeFieldStop(); + $protocol->writeStructEnd(); + } + ); + + $transport = new TMemoryBuffer($buffer); + $protocol = new TBinaryProtocol($transport); + + $this->assertSame(strlen($buffer), $protocol->skip(TType::STRUCT)); + $this->assertSame(0, (int)$transport->available()); + } + + public function testSkipMap(): void + { + $buffer = $this->buildBinaryBuffer( + function (TBinaryProtocol $protocol): void { + $protocol->writeMapBegin(TType::I32, TType::STRING, 2); + $protocol->writeI32(1); + $protocol->writeString('a'); + $protocol->writeI32(2); + $protocol->writeString('bc'); + $protocol->writeMapEnd(); + } + ); + + $transport = new TMemoryBuffer($buffer); + $protocol = new TBinaryProtocol($transport); + + $this->assertSame(strlen($buffer), $protocol->skip(TType::MAP)); + $this->assertSame(0, (int)$transport->available()); + } + + public function testSkipSet(): void + { + $buffer = $this->buildBinaryBuffer( + function (TBinaryProtocol $protocol): void { + $protocol->writeSetBegin(TType::I16, 2); + $protocol->writeI16(10); + $protocol->writeI16(20); + $protocol->writeSetEnd(); + } + ); + + $transport = new TMemoryBuffer($buffer); + $protocol = new TBinaryProtocol($transport); + + $this->assertSame(strlen($buffer), $protocol->skip(TType::SET)); + $this->assertSame(0, (int)$transport->available()); + } + + public function testSkipList(): void + { + $buffer = $this->buildBinaryBuffer( + function (TBinaryProtocol $protocol): void { + $protocol->writeListBegin(TType::STRING, 2); + $protocol->writeString('first'); + $protocol->writeString('second'); + $protocol->writeListEnd(); + } + ); + + $transport = new TMemoryBuffer($buffer); + $protocol = new TBinaryProtocol($transport); + + $this->assertSame(strlen($buffer), $protocol->skip(TType::LST)); + $this->assertSame(0, (int)$transport->available()); + } + + public function testSkipThrowsForUnknownType(): void + { + $protocol = new TBinaryProtocol(new TMemoryBuffer()); + + $this->expectException(TProtocolException::class); + $this->expectExceptionCode(TProtocolException::INVALID_DATA); + + $protocol->skip(999); + } + + /** + * @dataProvider skipScalarDataProvider + */ + public function testSkipBinaryScalarValues(int $type, string $writerMethod, $value): void + { + $buffer = $this->buildBinaryBuffer( + function (TBinaryProtocol $protocol) use ($writerMethod, $value): void { + $protocol->{$writerMethod}($value); + } + ); + + $this->assertSkipBinaryConsumesBuffer($buffer, $type); + } + + public function testSkipBinaryStruct(): void + { + $buffer = $this->buildBinaryBuffer( + function (TBinaryProtocol $protocol): void { + $protocol->writeStructBegin('Example'); + $protocol->writeFieldBegin('flag', TType::BOOL, 1); + $protocol->writeBool(true); + $protocol->writeFieldEnd(); + $protocol->writeFieldBegin('name', TType::STRING, 2); + $protocol->writeString('value'); + $protocol->writeFieldEnd(); + $protocol->writeFieldStop(); + $protocol->writeStructEnd(); + } + ); + + $this->assertSkipBinaryConsumesBuffer($buffer, TType::STRUCT); + } + + public function testSkipBinaryMap(): void + { + $buffer = $this->buildBinaryBuffer( + function (TBinaryProtocol $protocol): void { + $protocol->writeMapBegin(TType::I32, TType::STRING, 2); + $protocol->writeI32(1); + $protocol->writeString('a'); + $protocol->writeI32(2); + $protocol->writeString('bc'); + $protocol->writeMapEnd(); + } + ); + + $this->assertSkipBinaryConsumesBuffer($buffer, TType::MAP); + } + + public function testSkipBinaryList(): void + { + $buffer = $this->buildBinaryBuffer( + function (TBinaryProtocol $protocol): void { + $protocol->writeListBegin(TType::STRING, 2); + $protocol->writeString('first'); + $protocol->writeString('second'); + $protocol->writeListEnd(); + } + ); + + $this->assertSkipBinaryConsumesBuffer($buffer, TType::LST); + } + + public function testSkipBinarySet(): void + { + $buffer = $this->buildBinaryBuffer( + function (TBinaryProtocol $protocol): void { + $protocol->writeSetBegin(TType::DOUBLE, 2); + $protocol->writeDouble(3.14); + $protocol->writeDouble(6.28); + $protocol->writeSetEnd(); + } + ); + + $this->assertSkipBinaryConsumesBuffer($buffer, TType::SET); + } + + public function testSkipBinaryThrowsForUnknownType(): void + { + $this->expectException(TProtocolException::class); + $this->expectExceptionCode(TProtocolException::INVALID_DATA); + + TProtocol::skipBinary(new TMemoryBuffer(), 999); + } + + private function buildBinaryBuffer(callable $writer): string + { + $transport = new TMemoryBuffer(); + $protocol = new TBinaryProtocol($transport); + $writer($protocol); + + return $transport->getBuffer(); + } + + private function assertSkipBinaryConsumesBuffer(string $buffer, int $type): void + { + $transport = new TMemoryBuffer($buffer); + + $this->assertSame(strlen($buffer), TProtocol::skipBinary($transport, $type)); + $this->assertSame(0, (int)$transport->available()); + } +} diff --git a/lib/php/test/Unit/Lib/Server/Fixture/ServerStub.php b/lib/php/test/Unit/Lib/Server/Fixture/ServerStub.php new file mode 100644 index 00000000000..ea8749bbee7 --- /dev/null +++ b/lib/php/test/Unit/Lib/Server/Fixture/ServerStub.php @@ -0,0 +1,35 @@ +acceptedTransport = $acceptedTransport; + } + + public function listen() + { + } + + public function close() + { + } + + protected function acceptImpl() + { + return $this->acceptedTransport; + } +} diff --git a/lib/php/test/Unit/Lib/Server/TServerTest.php b/lib/php/test/Unit/Lib/Server/TServerTest.php new file mode 100644 index 00000000000..70ff39776ad --- /dev/null +++ b/lib/php/test/Unit/Lib/Server/TServerTest.php @@ -0,0 +1,60 @@ +createMock(TServerTransport::class); + $inputTransportFactory = $this->createMock(TTransportFactoryInterface::class); + $outputTransportFactory = $this->createMock(TTransportFactoryInterface::class); + $inputProtocolFactory = $this->createMock(TProtocolFactory::class); + $outputProtocolFactory = $this->createMock(TProtocolFactory::class); + + $server = new ServerStub( + $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_')); + } +} diff --git a/lib/php/test/Unit/Lib/Server/TServerTransportTest.php b/lib/php/test/Unit/Lib/Server/TServerTransportTest.php new file mode 100644 index 00000000000..3e99f5f6426 --- /dev/null +++ b/lib/php/test/Unit/Lib/Server/TServerTransportTest.php @@ -0,0 +1,48 @@ +createMock(TTransport::class); + $serverTransport = new ServerTransportStub($transport); + + $this->assertSame($transport, $serverTransport->accept()); + } + + public function testAcceptRejectsNullTransport(): void + { + $serverTransport = new ServerTransportStub(null); + + $this->expectException(TTransportException::class); + $this->expectExceptionMessage('accept() may not return NULL'); + + $serverTransport->accept(); + } +} diff --git a/lib/php/test/Unit/Lib/StoredMessageProtocolTest.php b/lib/php/test/Unit/Lib/StoredMessageProtocolTest.php new file mode 100644 index 00000000000..88089146070 --- /dev/null +++ b/lib/php/test/Unit/Lib/StoredMessageProtocolTest.php @@ -0,0 +1,52 @@ +createMock(TTransport::class); + $protocol = $this->createMock(TProtocol::class); + $protocol->expects($this->once()) + ->method('getTransport') + ->willReturn($transport); + + $stored = new StoredMessageProtocol($protocol, 'ping', TMessageType::ONEWAY, 99); + + $name = ''; + $type = 0; + $seqid = 0; + $stored->readMessageBegin($name, $type, $seqid); + + $this->assertSame('ping', $name); + $this->assertSame(TMessageType::ONEWAY, $type); + $this->assertSame(99, $seqid); + $this->assertSame($transport, $stored->getTransport()); + } +} diff --git a/lib/php/test/Unit/Lib/TMultiplexedProcessorTest.php b/lib/php/test/Unit/Lib/TMultiplexedProcessorTest.php new file mode 100644 index 00000000000..83bebe50d70 --- /dev/null +++ b/lib/php/test/Unit/Lib/TMultiplexedProcessorTest.php @@ -0,0 +1,146 @@ +createMock(TTransport::class); + $input = $this->createMock(TProtocol::class); + $output = $this->createMock(TProtocol::class); + $processor = $this->createMock(ProcessorSpy::class); + + $input->expects($this->once()) + ->method('readMessageBegin') + ->willReturnCallback(function (&$name, &$type, &$seqid): int { + $name = 'Calculator:add'; + $type = TMessageType::CALL; + $seqid = 42; + + return 0; + }); + $input->expects($this->once()) + ->method('getTransport') + ->willReturn($transport); + + $processor->expects($this->once()) + ->method('process') + ->with( + $this->isInstanceOf(StoredMessageProtocol::class), + $this->identicalTo($output) + ) + ->willReturnCallback(function (StoredMessageProtocol $decoratedInput, TProtocol $decoratedOutput) use ($output) { + $name = ''; + $type = 0; + $seqid = 0; + $decoratedInput->readMessageBegin($name, $type, $seqid); + + TestCase::assertSame('add', $name); + TestCase::assertSame(TMessageType::CALL, $type); + TestCase::assertSame(42, $seqid); + TestCase::assertSame($output, $decoratedOutput); + + return 'processed'; + }); + + $multiplexedProcessor = new TMultiplexedProcessor(); + $multiplexedProcessor->registerProcessor('Calculator', $processor); + + $this->assertSame('processed', $multiplexedProcessor->process($input, $output)); + } + + public function testProcessRejectsUnexpectedMessageType(): void + { + $input = $this->createMock(TProtocol::class); + $output = $this->createMock(TProtocol::class); + + $input->expects($this->once()) + ->method('readMessageBegin') + ->willReturnCallback(function (&$name, &$type, &$seqid): int { + $name = 'Calculator:add'; + $type = TMessageType::REPLY; + $seqid = 1; + + return 0; + }); + + $this->expectException(TException::class); + $this->expectExceptionMessage('This should not have happened!?'); + + (new TMultiplexedProcessor())->process($input, $output); + } + + public function testProcessRequiresServiceSeparator(): void + { + $input = $this->createMock(TProtocol::class); + $output = $this->createMock(TProtocol::class); + + $input->expects($this->once()) + ->method('readMessageBegin') + ->willReturnCallback(function (&$name, &$type, &$seqid): int { + $name = 'add'; + $type = TMessageType::CALL; + $seqid = 1; + + return 0; + }); + + $this->expectException(TException::class); + $this->expectExceptionMessage('Service name not found in message name: add.'); + + (new TMultiplexedProcessor())->process($input, $output); + } + + public function testProcessRequiresKnownServiceName(): void + { + $input = $this->createMock(TProtocol::class); + $output = $this->createMock(TProtocol::class); + + $input->expects($this->once()) + ->method('readMessageBegin') + ->willReturnCallback(function (&$name, &$type, &$seqid): int { + $name = 'Missing:add'; + $type = TMessageType::CALL; + $seqid = 1; + + return 0; + }); + + $multiplexedProcessor = new TMultiplexedProcessor(); + $multiplexedProcessor->registerProcessor('Other', $this->createMock(ProcessorSpy::class)); + + $this->expectException(TException::class); + $this->expectExceptionMessage('Service name not found: Missing.'); + + $multiplexedProcessor->process($input, $output); + } +} diff --git a/lib/php/test/Unit/Lib/Transport/Fixture/BufferedReadTransport.php b/lib/php/test/Unit/Lib/Transport/Fixture/BufferedReadTransport.php new file mode 100644 index 00000000000..2cc6629355a --- /dev/null +++ b/lib/php/test/Unit/Lib/Transport/Fixture/BufferedReadTransport.php @@ -0,0 +1,59 @@ +chunks = $chunks; + } + + public function isOpen() + { + return true; + } + + public function open() + { + } + + public function close() + { + } + + public function read($len) + { + $this->readRequests[] = $len; + + return array_shift($this->chunks); + } + + public function write($buf) + { + } +} diff --git a/lib/php/test/Unit/Lib/Transport/TTransportTest.php b/lib/php/test/Unit/Lib/Transport/TTransportTest.php new file mode 100644 index 00000000000..60bb92b5bf1 --- /dev/null +++ b/lib/php/test/Unit/Lib/Transport/TTransportTest.php @@ -0,0 +1,43 @@ +assertSame('abcd', $transport->readAll(4)); + $this->assertSame([4, 2], $transport->readRequests); + } + + public function testFlushIsNoOp(): void + { + $transport = new BufferedReadTransport([]); + + $this->assertNull($transport->flush()); + } +} diff --git a/lib/php/test/Unit/Lib/Type/Fixture/CachedConstantStub.php b/lib/php/test/Unit/Lib/Type/Fixture/CachedConstantStub.php new file mode 100644 index 00000000000..0ccfb0d05c8 --- /dev/null +++ b/lib/php/test/Unit/Lib/Type/Fixture/CachedConstantStub.php @@ -0,0 +1,41 @@ +value = static::$initCalls; + + return $value; + } +} diff --git a/lib/php/test/Unit/Lib/Type/TypeConstantsTest.php b/lib/php/test/Unit/Lib/Type/TypeConstantsTest.php new file mode 100644 index 00000000000..4d27edf0440 --- /dev/null +++ b/lib/php/test/Unit/Lib/Type/TypeConstantsTest.php @@ -0,0 +1,70 @@ +assertSame(1, CachedConstantStub::$initCalls); + $this->assertSame($first, $second); + $this->assertSame(1, $first->value); + } + + public function testMessageTypeConstantsRemainStable(): void + { + $this->assertSame(1, TMessageType::CALL); + $this->assertSame(2, TMessageType::REPLY); + $this->assertSame(3, TMessageType::EXCEPTION); + $this->assertSame(4, TMessageType::ONEWAY); + } + + public function testTypeConstantsRemainStable(): void + { + $this->assertSame(0, TType::STOP); + $this->assertSame(1, TType::VOID); + $this->assertSame(2, TType::BOOL); + $this->assertSame(3, TType::BYTE); + $this->assertSame(TType::BYTE, TType::I08); + $this->assertSame(4, TType::DOUBLE); + $this->assertSame(6, TType::I16); + $this->assertSame(8, TType::I32); + $this->assertSame(10, TType::I64); + $this->assertSame(11, TType::STRING); + $this->assertSame(TType::STRING, TType::UTF7); + $this->assertSame(12, TType::STRUCT); + $this->assertSame(13, TType::MAP); + $this->assertSame(14, TType::SET); + $this->assertSame(15, TType::LST); + $this->assertSame(16, TType::UUID); + $this->assertSame(TType::UUID, TType::UTF8); + $this->assertSame(17, TType::UTF16); + } +} diff --git a/lib/php/test/bootstrap.php b/lib/php/test/bootstrap.php index c6291e51798..f3c37ba89c6 100644 --- a/lib/php/test/bootstrap.php +++ b/lib/php/test/bootstrap.php @@ -6,6 +6,7 @@ $loader = new ThriftClassLoader(); $loader->registerNamespace('Basic', __DIR__ . '/Resources/packages/php'); +$loader->registerNamespace('BasicInline', __DIR__ . '/Resources/packages/phpi'); $loader->registerNamespace('Validate', __DIR__ . '/Resources/packages/phpv'); $loader->registerNamespace('ValidateOop', __DIR__ . '/Resources/packages/phpvo'); $loader->registerNamespace('Json', __DIR__ . '/Resources/packages/phpjs');