2020-10-02 20:36:23 +00:00
|
|
|
<?php
|
|
|
|
|
|
2020-10-28 20:51:19 +00:00
|
|
|
// phpcs:disable MediaWiki.Commenting.FunctionComment.ObjectTypeHintParam
|
2021-02-01 19:38:05 +00:00
|
|
|
// phpcs:disable MediaWiki.Commenting.FunctionComment.MissingParamTag -- Traits are not excluded
|
2020-10-28 20:51:19 +00:00
|
|
|
|
2020-11-05 17:04:37 +00:00
|
|
|
namespace Wikimedia\Tests;
|
2020-10-02 20:36:23 +00:00
|
|
|
|
|
|
|
|
use Generator;
|
|
|
|
|
use ReflectionClass;
|
|
|
|
|
|
|
|
|
|
trait SerializationTestTrait {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Data provider for deserialization test.
|
|
|
|
|
* - For each supported serialization format defined by ::getSupportedSerializationFormats
|
|
|
|
|
* - For each acceptance test instance defined by ::getTestInstancesAndAssertions
|
|
|
|
|
* - For each object deserialized from stored file for a particular MW version
|
2020-11-11 19:42:55 +00:00
|
|
|
* @return Generator for [ callable $deserializer, object $expectedObject, string $dataToDeserialize ]
|
2020-10-02 20:36:23 +00:00
|
|
|
*/
|
|
|
|
|
public function provideTestDeserialization(): Generator {
|
|
|
|
|
$className = $this->getClassToTest();
|
|
|
|
|
foreach ( $this->getSupportedSerializationFormats() as $serializationFormat ) {
|
|
|
|
|
$serializationUtils = new SerializationTestUtils(
|
|
|
|
|
$this->getSerializedDataPath(),
|
|
|
|
|
$this->getTestInstances(),
|
|
|
|
|
$serializationFormat['ext'],
|
|
|
|
|
$serializationFormat['serializer'],
|
|
|
|
|
$serializationFormat['deserializer']
|
|
|
|
|
);
|
|
|
|
|
foreach ( $serializationUtils->getTestInstances() as $testCaseName => $expectedObject ) {
|
2020-11-11 19:42:55 +00:00
|
|
|
$deserializationFixtures = $serializationUtils->getFixturesForTestCase(
|
2020-10-02 20:36:23 +00:00
|
|
|
$className,
|
|
|
|
|
$testCaseName
|
|
|
|
|
);
|
2020-11-11 19:42:55 +00:00
|
|
|
foreach ( $deserializationFixtures as $deserializedObjectInfo ) {
|
2020-10-02 20:36:23 +00:00
|
|
|
yield "{$className}:{$testCaseName}, " .
|
|
|
|
|
"deserialized from {$deserializedObjectInfo->ext}, " .
|
2020-11-11 19:42:55 +00:00
|
|
|
"{$deserializedObjectInfo->version}" =>
|
|
|
|
|
[ $serializationFormat['deserializer'], $expectedObject, $deserializedObjectInfo->data ];
|
2020-10-02 20:36:23 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that $deserialized objects retrieved from stored files for various MW versions
|
|
|
|
|
* equal to the $expected
|
|
|
|
|
* @dataProvider provideTestDeserialization
|
|
|
|
|
*/
|
2020-11-11 19:42:55 +00:00
|
|
|
public function testDeserialization( callable $deserializer, object $expected, string $data ) {
|
|
|
|
|
$deserialized = $deserializer( $data );
|
2020-10-02 20:36:23 +00:00
|
|
|
$this->assertInstanceOf( $this->getClassToTest(), $deserialized );
|
|
|
|
|
$this->validateObjectEquality( $expected, $deserialized );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Data provider for serialization test.
|
|
|
|
|
* - For each supported serialization format defined by ::getSupportedSerializationFormats
|
|
|
|
|
* - For each acceptance test instance defined by ::getTestInstancesAndAssertions
|
2020-11-11 19:42:55 +00:00
|
|
|
* @return Generator for [ callable $serializer, string $expectedSerialization, object $testInstanceToSerialize ]
|
2020-10-02 20:36:23 +00:00
|
|
|
*/
|
|
|
|
|
public function provideSerialization(): Generator {
|
|
|
|
|
$className = $this->getClassToTest();
|
|
|
|
|
foreach ( $this->getSupportedSerializationFormats() as $serializationFormat ) {
|
|
|
|
|
$serializationUtils = new SerializationTestUtils(
|
|
|
|
|
$this->getSerializedDataPath(),
|
|
|
|
|
$this->getTestInstances(),
|
|
|
|
|
$serializationFormat['ext'],
|
|
|
|
|
$serializationFormat['serializer'],
|
|
|
|
|
$serializationFormat['deserializer']
|
|
|
|
|
);
|
2021-09-03 22:52:31 +00:00
|
|
|
foreach ( $serializationUtils->getTestInstances() as $testCaseName => $testInstance ) {
|
2020-10-02 20:36:23 +00:00
|
|
|
$expected = $serializationUtils->getStoredSerializedInstance( $className, $testCaseName );
|
2020-11-03 10:58:00 +00:00
|
|
|
|
|
|
|
|
if ( $expected->data === null ) {
|
|
|
|
|
// The fixture file is missing. This will be detected and reported elsewhere.
|
|
|
|
|
// No need to cause an error here.
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-02 20:36:23 +00:00
|
|
|
yield "{$className}:{$testCaseName}, " .
|
2020-11-11 19:42:55 +00:00
|
|
|
"serialized with {$serializationFormat['ext']}" =>
|
|
|
|
|
[ $serializationFormat['serializer'], $expected->data, $testInstance ];
|
2020-10-02 20:36:23 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test that the current master $serialized instances are equal to stored $expected instances.
|
|
|
|
|
* @dataProvider provideSerialization
|
|
|
|
|
*/
|
2020-11-11 19:42:55 +00:00
|
|
|
public function testSerialization( callable $serializer, string $expected, object $testInstance ) {
|
|
|
|
|
$this->assertSame( $expected, $serializer( $testInstance ) );
|
2020-10-02 20:36:23 +00:00
|
|
|
}
|
|
|
|
|
|
2020-09-29 19:18:00 +00:00
|
|
|
/**
|
|
|
|
|
* Data provider for serialization round trip test.
|
|
|
|
|
* - For each supported serialization format defined by ::getSupportedSerializationFormats
|
|
|
|
|
* - For each test instance defined by ::getTestInstances
|
|
|
|
|
* @return Generator for [ object $instance, callable $serializer, callable $deserializer ]
|
|
|
|
|
*/
|
|
|
|
|
public function provideSerializationRoundTrip(): Generator {
|
|
|
|
|
$className = $this->getClassToTest();
|
|
|
|
|
foreach ( $this->getSupportedSerializationFormats() as $serializationFormat ) {
|
|
|
|
|
foreach ( $this->getTestInstances() as $testCaseName => $instance ) {
|
|
|
|
|
yield "{$className}:{$testCaseName}, " .
|
|
|
|
|
"serialized with {$serializationFormat['ext']}" => [
|
|
|
|
|
$instance,
|
|
|
|
|
$serializationFormat['serializer'],
|
|
|
|
|
$serializationFormat['deserializer']
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test that the $expected instance can be serialized and successfully be deserialized again.
|
|
|
|
|
*
|
|
|
|
|
* @dataProvider provideSerializationRoundTrip
|
|
|
|
|
*/
|
|
|
|
|
public function testSerializationRoundTrip(
|
|
|
|
|
object $instance,
|
|
|
|
|
callable $serializer,
|
|
|
|
|
callable $deserializer
|
|
|
|
|
) {
|
|
|
|
|
$blob = $serializer( $instance );
|
|
|
|
|
$this->assertNotEmpty( $blob );
|
|
|
|
|
|
|
|
|
|
$actual = $deserializer( $blob );
|
|
|
|
|
$this->assertNotEmpty( $actual );
|
|
|
|
|
|
|
|
|
|
$this->validateObjectEquality( $instance, $actual );
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-02 20:36:23 +00:00
|
|
|
/**
|
|
|
|
|
* Asserts that all the fields across class hierarchy for
|
|
|
|
|
* provided objects are equal.
|
|
|
|
|
* @param object $expected
|
|
|
|
|
* @param object $actual
|
|
|
|
|
* @param ReflectionClass|null $class
|
|
|
|
|
*/
|
|
|
|
|
private function validateObjectEquality(
|
|
|
|
|
object $expected,
|
|
|
|
|
object $actual,
|
|
|
|
|
ReflectionClass $class = null
|
|
|
|
|
) {
|
|
|
|
|
if ( !$class ) {
|
|
|
|
|
$class = new ReflectionClass( $expected );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $class->getProperties() as $prop ) {
|
|
|
|
|
$prop->setAccessible( true );
|
|
|
|
|
$expectedValue = $prop->getValue( $expected );
|
|
|
|
|
$actualValue = $prop->getValue( $actual );
|
2020-11-03 10:58:00 +00:00
|
|
|
$this->assertSame( $expectedValue, $actualValue, $prop->getName() );
|
2020-10-02 20:36:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$parent = $class->getParentClass();
|
|
|
|
|
if ( $parent ) {
|
|
|
|
|
$this->validateObjectEquality( $expected, $actual, $parent );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Data provider for acceptance testing, returning object instances created by current code.
|
|
|
|
|
* - For each acceptance test instance defined by ::getTestInstancesAndAssertions
|
|
|
|
|
* @return Generator for [ $instance which to run assertions on, $assertionsCallback ]
|
|
|
|
|
*/
|
|
|
|
|
public function provideCurrentVersionTestObjects(): Generator {
|
|
|
|
|
$className = $this->getClassToTest();
|
|
|
|
|
$testCases = $this->getTestInstancesAndAssertions();
|
|
|
|
|
foreach ( $testCases as $testCaseName => $testCase ) {
|
|
|
|
|
yield "{$className}:{$testCaseName}, current" =>
|
|
|
|
|
[ $testCase['instance'], $testCase['assertions'] ];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Data provider for acceptance testing, returning instances deserialized
|
|
|
|
|
* from stored files for various MW versions.
|
|
|
|
|
* - For each supported serialization format defined by ::getSupportedSerializationFormats
|
|
|
|
|
* - For each object deserialized from stored file for a particular MW version
|
|
|
|
|
* @return Generator for [ $instance which to run assertions on, $assertionsCallback ]
|
|
|
|
|
*/
|
|
|
|
|
public function provideDeserializedTestObjects(): Generator {
|
|
|
|
|
$className = $this->getClassToTest();
|
|
|
|
|
$testCases = $this->getTestInstancesAndAssertions();
|
2021-02-06 19:30:20 +00:00
|
|
|
$testObjects = array_map( static function ( $testCase ) {
|
2020-10-02 20:36:23 +00:00
|
|
|
return $testCase['instance'];
|
|
|
|
|
}, $testCases );
|
|
|
|
|
foreach ( $this->getSupportedSerializationFormats() as $serializationFormat ) {
|
|
|
|
|
$serializationUtils = new SerializationTestUtils(
|
|
|
|
|
$this->getSerializedDataPath(),
|
|
|
|
|
$testObjects,
|
|
|
|
|
$serializationFormat['ext'],
|
|
|
|
|
$serializationFormat['serializer'],
|
|
|
|
|
$serializationFormat['deserializer']
|
|
|
|
|
);
|
|
|
|
|
foreach ( array_keys( $testObjects ) as $testCaseName ) {
|
|
|
|
|
$deserializedObjects = $serializationUtils->getDeserializedInstancesForTestCase(
|
|
|
|
|
$className,
|
|
|
|
|
$testCaseName
|
|
|
|
|
);
|
|
|
|
|
foreach ( $deserializedObjects as $deserializedObjectInfo ) {
|
|
|
|
|
yield "{$className}:{$testCaseName}, " .
|
|
|
|
|
"deserialized from {$deserializedObjectInfo->ext}, " .
|
|
|
|
|
"{$deserializedObjectInfo->version}" =>
|
|
|
|
|
[
|
|
|
|
|
$deserializedObjectInfo->object,
|
|
|
|
|
$testCases[ $testCaseName ]['assertions']
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that assertions in $assertionsCallback succeed on $testInstance.
|
|
|
|
|
* @see self::getTestInstancesAndAssertions()
|
|
|
|
|
* @dataProvider provideDeserializedTestObjects
|
|
|
|
|
* @dataProvider provideCurrentVersionTestObjects
|
|
|
|
|
*/
|
|
|
|
|
public function testAcceptanceOfDeserializedInstances(
|
|
|
|
|
object $testInstance,
|
|
|
|
|
callable $assertionsCallback
|
|
|
|
|
) {
|
|
|
|
|
call_user_func( $assertionsCallback, $this, $testInstance );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns a map of $testCaseName to an instance to test.
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
private function getTestInstances(): array {
|
2021-02-06 19:30:20 +00:00
|
|
|
return array_map( static function ( $testCase ) {
|
2020-10-02 20:36:23 +00:00
|
|
|
return $testCase['instance'];
|
|
|
|
|
}, $this->getTestInstancesAndAssertions() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return string the name of the class to test.
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function getClassToTest(): string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return string the path to serialized data.
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function getSerializedDataPath(): string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array a map of $testCaseName to a map, containing the following keys:
|
|
|
|
|
* - 'instance' => an instance of the object to perform assertions on.
|
|
|
|
|
* - 'assertions' => a callable that performs assertions on the deserialized objects.
|
|
|
|
|
* Callable signature: ( MediaWikiIntegrationTestCase $testCase, object $instance )
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function getTestInstancesAndAssertions(): array;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a list of serialization formats supported by the tested class.
|
|
|
|
|
* @return string[][] a list of supported serialization formats info map,
|
|
|
|
|
* containing the following keys:
|
|
|
|
|
* - 'ext' => string file extension for stored serializations
|
|
|
|
|
* - 'serializer' => callable to serialize objects
|
|
|
|
|
* - 'deserializer' => callable to deserialize objects
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function getSupportedSerializationFormats(): array;
|
|
|
|
|
}
|