aboutsummaryrefslogtreecommitdiffstats
path: root/tests/phpunit/unit/includes/json/JsonCodecTest.php
diff options
context:
space:
mode:
authorC. Scott Ananian <cscott@cscott.net>2023-11-17 13:31:19 -0500
committerJames D. Forrester <jforrester@wikimedia.org>2024-10-15 20:09:51 -0400
commit3bc172d0e4e8048a415b6992af3b6db84929cc02 (patch)
treeb3785adc7fd7e9acf0817480c2d4e250702dcae3 /tests/phpunit/unit/includes/json/JsonCodecTest.php
parent1b65e8ba83db8158a5a946b9dd006718520d4f68 (diff)
downloadmediawikicore-3bc172d0e4e8048a415b6992af3b6db84929cc02.tar.gz
mediawikicore-3bc172d0e4e8048a415b6992af3b6db84929cc02.zip
[JsonCodec] Use wikimedia/json-codec to implement JsonCodec
This adds support for serializing/deserializing objects which implement the JsonCodecable interface from the wikimedia/json-codec library used by Parsoid. JsonCodecable allows customizing the encoding of objects of a given class using a class-specific codec object, and JsonCodecable is an interface which is defined and can be used outside mediawiki core. In addition json-codec supports deserialization in the presence of aliased class names, fixing T353883. Backward and forward compatibility established via the mechanism described in https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility Test data generated by this patch was added in I109640b510cef9b3b870a8c188f3b4f086d75d06 to ensure forward compatibility with the output after this patch is merged. Benchmarks: PHP 7.4.33 PHP 8.2.19 PHP 8.3.6 BEFORE AFTER BEFORE AFTER BEFORE AFTER Serialize: 926.7/s 1424.8/s 978.5/s 1542.4/s 1023.5/s 1488.6/s Serialize (assoc): 930.2/s 1378.6/s 974.6/s 1541.9/s 1022.4/s 1463.4/s Deserialize: 1942.7/s 1961.3/s 2118.8/s 2175.9/s 2129.8/s 2063.5/s Deserialize (assoc): 1952.0/s 1905.7/s 2107.5/s 2192.1/s 2153.3/s 2011.1/s These numbers definitely do not have as many significant digits as written here. But they should be sufficient to demonstrate that performance is not impaired by this patch and in fact serialization speed improves slightly. Bug: T273540 Bug: T327439 Bug: T346829 Bug: T353883 Depends-On: If1d70ba18712839615c1f4fea236843ffebc8645 Change-Id: Ia1017dcef462f3ac1ff5112106f7df81f5cc384f
Diffstat (limited to 'tests/phpunit/unit/includes/json/JsonCodecTest.php')
-rw-r--r--tests/phpunit/unit/includes/json/JsonCodecTest.php101
1 files changed, 93 insertions, 8 deletions
diff --git a/tests/phpunit/unit/includes/json/JsonCodecTest.php b/tests/phpunit/unit/includes/json/JsonCodecTest.php
index 2854e4e87bf9..cbc50bc65045 100644
--- a/tests/phpunit/unit/includes/json/JsonCodecTest.php
+++ b/tests/phpunit/unit/includes/json/JsonCodecTest.php
@@ -11,6 +11,7 @@ use MediaWiki\Json\JsonConstants;
use MediaWiki\Title\Title;
use MediaWiki\User\UserIdentityValue;
use MediaWikiUnitTestCase;
+use Psr\Container\ContainerInterface;
use Wikimedia\Assert\PreconditionException;
/**
@@ -19,8 +20,35 @@ use Wikimedia\Assert\PreconditionException;
*/
class JsonCodecTest extends MediaWikiUnitTestCase {
+ /** Mock up a services interface. */
+ private static function getServices(): ContainerInterface {
+ static $services = null;
+ if ( !$services ) {
+ $services = new class implements ContainerInterface {
+ private $storage = [];
+
+ public function get( $id ) {
+ return $this->storage[$id];
+ }
+
+ public function has( $id ): bool {
+ return isset( $this->storage[$id] );
+ }
+
+ public function set( $id, $value ) {
+ $this->storage[$id] = $value;
+ }
+ };
+ $factory = new ManagedObjectFactory();
+ $factory->create( "a", 1 );
+ $factory->create( "b", 2 );
+ $services->set( 'ManagedObjectFactory', $factory );
+ }
+ return $services;
+ }
+
private function getCodec(): JsonCodec {
- return new JsonCodec();
+ return new JsonCodec( self::getServices() );
}
public static function provideSimpleTypes() {
@@ -36,11 +64,11 @@ class JsonCodecTest extends MediaWikiUnitTestCase {
public static function provideInvalidJsonData() {
yield 'Bad string' => [ 'bad string' ];
- yield 'No unserialization metadata' => [ [ 'test' => 'test' ] ];
- yield 'Unserialization metadata, but class not exist' => [ [
+ yield 'No deserialization metadata' => [ [ 'test' => 'test' ] ];
+ yield 'Deserialization metadata, but class not exist' => [ [
JsonConstants::TYPE_ANNOTATION => 'BadClassNotExist'
] ];
- yield 'Unserialization metadata, but class is not JsonDeserializable' => [ [
+ yield 'Deserialization metadata, but class is not JsonDeserializable' => [ [
JsonConstants::TYPE_ANNOTATION => Title::class
] ];
}
@@ -74,7 +102,7 @@ class JsonCodecTest extends MediaWikiUnitTestCase {
$this->getCodec()->deserialize( $jsonData, JsonDeserializableSuperClass::class );
}
- public function testExpectedClassMustBeUnserializable() {
+ public function testExpectedClassMustBeDeserializable() {
$this->expectException( PreconditionException::class );
$this->getCodec()->deserialize( '{}', self::class );
}
@@ -113,6 +141,7 @@ class JsonCodecTest extends MediaWikiUnitTestCase {
$json = (object)$superClassInstance->jsonSerialize();
$superClassDeserialized = $this->getCodec()->deserialize( $json );
$this->assertInstanceOf( JsonDeserializableSuperClass::class, $superClassInstance );
+ $this->assertInstanceOf( JsonDeserializableSuperClass::class, $superClassDeserialized );
$this->assertSame( $superClassInstance->getSuperClassField(), $superClassDeserialized->getSuperClassField() );
}
@@ -148,6 +177,12 @@ class JsonCodecTest extends MediaWikiUnitTestCase {
new JsonDeserializableSubClass( 'Super Value', 'Sub Value' ),
42
];
+ // This is pretty bogus, in that it tests the ability of JsonCodec
+ // to decode something which *wasn't* generated by JsonCodec, but
+ // instead used only json_encode/JsonSerializable. Still this should
+ // work as long as JsonDeserializableTrait is used and the arrays
+ // returned contain only primitive types (ie, not nested
+ // JsonSerializables)
$serialized = FormatJson::encode( $array );
$deserialized = $this->getCodec()->deserializeArray( FormatJson::decode( $serialized ) );
$this->assertArrayEquals( $array, $deserialized );
@@ -188,14 +223,29 @@ class JsonCodecTest extends MediaWikiUnitTestCase {
[ [ new JsonDeserializableSuperClass( 'Test' ) ], true, null ];
yield 'JsonDeserializable instance, in stdClass' =>
[ (object)[ new JsonDeserializableSuperClass( 'Test' ) ], true, null ];
+ yield 'JsonDeserializable instance, in JsonCodecable' =>
+ [ new SampleContainerObject( new JsonDeserializableSuperClass( 'Test' ) ), true, null ];
yield 'JsonSerializable instance' => [ $serializableClass, false, null ];
yield 'JsonSerializable instance, in array' => [ [ $serializableClass ], false, null ];
yield 'JsonSerializable instance, in stdClass' => [ (object)[ $serializableClass ], false, null ];
+ yield 'JsonSerializable instance, in JsonCodecable' => [ new SampleContainerObject( $serializableClass ), false, null ];
yield 'JsonSerializable instance, expect deserialize' => [ $serializableClass, true, '$' ];
yield 'JsonSerializable instance, in array, expect deserialize' => [ [ $serializableClass ], true, '$.0' ];
yield 'JsonSerializable instance, in stdClass, expect deserialize' =>
[ (object)[ $serializableClass ], true, '$.0' ];
yield 'Bad JsonSerializable instance' => [ $badSerializable, false, '$' ];
+
+ yield 'JsonCodecable instance' => [ new SampleObject( 'a' ), true, null ];
+ yield 'JsonCodecable instance, in array' =>
+ [ [ new SampleObject( '123' ) ], true, null ];
+ yield 'JsonCodecable instance, in stdClass' =>
+ [ (object)[ new SampleObject( 'Test' ) ], true, null ];
+ yield 'JsonCodecable instance, in JsonCodecable' => [
+ new SampleContainerObject( new SampleObject( '123' ) ), true, null
+ ];
+ // Managed values
+ $factory = self::getServices()->get( 'ManagedObjectFactory' );
+ yield 'Managed instance' => [ $factory->lookup( 'a' ), true, null ];
}
/**
@@ -217,7 +267,6 @@ class JsonCodecTest extends MediaWikiUnitTestCase {
* @dataProvider provideValidateSerializable
* @covers \MediaWiki\Json\JsonCodec::detectNonSerializableData
* @covers \MediaWiki\Json\JsonCodec::detectNonSerializableDataInternal
- * @covers \MediaWiki\Json\JsonCodec::detectNonSerializableDataInternal
*/
public function testValidateSerializable2( $value, bool $expectDeserialize, ?string $result ) {
if ( $result !== null || !$expectDeserialize ) {
@@ -225,7 +274,7 @@ class JsonCodecTest extends MediaWikiUnitTestCase {
return;
}
// Sanity check by ensuring that "serializable" things actually
- // are unserializable w/o losing value or type
+ // are deserializable w/o losing value or type
$json = $this->getCodec()->serialize( $value );
$newValue = $this->getCodec()->deserialize( $json );
$this->assertEquals( $value, $newValue );
@@ -257,7 +306,15 @@ class JsonCodecTest extends MediaWikiUnitTestCase {
}
};
yield 'array' => [ [ 'a' => 'b' ], '{"a":"b"}' ];
- yield 'JsonSerializable' => [ $serializableInstance, '{"c":"d","_complex_":true}' ];
+ yield 'JsonSerializable' => [
+ $serializableInstance,
+ json_encode( [ "c" => "d", "_type_" => get_class( $serializableInstance ), "_complex_" => true ], JSON_UNESCAPED_SLASHES )
+ ];
+ yield 'JsonCodecable' => [ new SampleObject( 'a' ), json_encode( [
+ 'property' => 'a',
+ '_type_' => SampleObject::class,
+ '_complex_' => true,
+ ] ) ];
}
/**
@@ -267,4 +324,32 @@ class JsonCodecTest extends MediaWikiUnitTestCase {
public function testSerializeSuccess( $value, string $expected ) {
$this->assertSame( $expected, $this->getCodec()->serialize( $value ) );
}
+
+ public function testManagedObjects() {
+ $codec = $this->getCodec();
+ $factory = self::getServices()->get( 'ManagedObjectFactory' );
+ $a = $factory->lookup( 'a' );
+ $s = $codec->serialize( $a );
+ $v = $codec->deserialize( $s );
+ // Not just "equals", $v should be reference-identical to $a
+ $this->assertSame( $a, $v );
+ }
+
+ public function testCodecableAliases() {
+ $codec = $this->getCodec();
+ // Note that the class name in _type_ is an *alias*, not the
+ // *actual* class name.
+ $json = '{"property":"alias!","_type_":"MediaWiki\\\\Tests\\\\Json\\\\SampleObjectAlias","_complex_":true}';
+ $v = $codec->deserialize( $json, SampleObject::class );
+ $this->assertInstanceOf( SampleObject::class, $v );
+ }
+
+ public function testJsonDeserializableAliases() {
+ $codec = $this->getCodec();
+ // Note that the class name in _type_ is an *alias*, not the
+ // *actual* class name.
+ $json = '{"super_class_field":1,"sub_class_field":"2","_type_":"MediaWiki\\\\Tests\\\\Json\\\\JsonDeserializableSubClassAlias","_complex_":true}';
+ $v = $codec->deserialize( $json, JsonDeserializableSubClass::class );
+ $this->assertInstanceOf( JsonDeserializableSubClass::class, $v );
+ }
}