Forward-compat for merging CacheTime and ParserOutput mOptions

CacheTime::mUsedOptions and ParserOutput::mAccessedOptions
do exactly the same thing and has to be merged into a single property.
This patch adds forward-compatibility and needs to be deployed
at least one train before the patch which actually merges the properties.

Change-Id: Ic9d71a443994e2545ebf2a826b9155c82961cb88
This commit is contained in:
Petr Pchelko 2020-11-05 10:04:37 -07:00
parent 8eafad0cdd
commit 017cfcf016
21 changed files with 215 additions and 13 deletions

View file

@ -1886,6 +1886,7 @@ $wgAutoloadLocalClasses = [
'Wikimedia\\Rdbms\\TimestampType' => __DIR__ . '/includes/libs/rdbms/database/TimestampType.php',
'Wikimedia\\Rdbms\\TinyIntType' => __DIR__ . '/includes/libs/rdbms/database/TinyIntType.php',
'Wikimedia\\Rdbms\\TransactionProfiler' => __DIR__ . '/includes/libs/rdbms/TransactionProfiler.php',
'Wikimedia\\Reflection\\GhostFieldAccessTrait' => __DIR__ . '/includes/libs/GhostFieldAccessTrait.php',
'WikitextContent' => __DIR__ . '/includes/content/WikitextContent.php',
'WikitextContentHandler' => __DIR__ . '/includes/content/WikitextContentHandler.php',
'WikitextLogFormatter' => __DIR__ . '/includes/logging/WikitextLogFormatter.php',

View file

@ -0,0 +1,69 @@
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace Wikimedia\Reflection;
/**
* Trait for accessing "ghost fields".
*
* Ghost fields are fields that have been created in an object instance
* by PHP's unserialize() mechanism, though they no longer exist
* in the current version of the corresponding class.
*
* Accessing non-public ghost fields offers some challenges due to
* how they are handled by PHP internally.
*
* @see https://www.php.net/manual/en/language.types.array.php#language.types.array.casting
* @since 1.36
*/
trait GhostFieldAccessTrait {
/**
* Get the value of the ghost field named $name,
* or null if the field does not exist.
*
* @param string $name
* @return mixed|null
*/
private function getGhostFieldValue( string $name ) {
if ( isset( $this->$name ) ) {
return $this->$name;
}
$data = (array)$this;
// Protected variables have a '*' prepended to the variable name.
// These prepended values have null bytes on either side.
$protectedName = "\x00*\x00{$name}";
if ( isset( $data[$protectedName] ) ) {
return $data[$protectedName];
}
// Private variables have the class name prepended to the variable name.
// These prepended values have null bytes on either side.
$thisClass = get_class( $this );
$privateName = "\x00{$thisClass}\x00{$name}";
if ( isset( $data[$privateName] ) ) {
return $data[$privateName];
}
return null;
}
}

View file

@ -22,6 +22,7 @@
*/
use MediaWiki\Parser\ParserCacheMetadata;
use Wikimedia\Reflection\GhostFieldAccessTrait;
/**
* Parser cache specific expiry check.
@ -29,6 +30,7 @@ use MediaWiki\Parser\ParserCacheMetadata;
* @ingroup Parser
*/
class CacheTime implements ParserCacheMetadata, JsonSerializable {
use GhostFieldAccessTrait;
/**
* @var string[] ParserOptions which have been taken into account to produce output.
@ -264,10 +266,22 @@ class CacheTime implements ParserCacheMetadata, JsonSerializable {
* @param array $jsonData
*/
protected function initFromJson( array $jsonData ) {
$this->mUsedOptions = $jsonData['UsedOptions'];
if ( array_key_exists( 'ParseUsedOptions', $jsonData ) ) {
// Forward compatibility
$this->mUsedOptions = array_keys( $jsonData['ParseUsedOptions'] ?: [] );
} else {
$this->mUsedOptions = $jsonData['UsedOptions'];
}
$this->mCacheExpiry = $jsonData['CacheExpiry'];
$this->mCacheTime = $jsonData['CacheTime'];
$this->mCacheRevisionId = $jsonData['CacheRevisionId'];
$this->mVersion = $jsonData['Version']; // XXX: we can probably remove this
}
public function __wakeup() {
$forwardCompatOptions = $this->getGhostFieldValue( 'mParseUsedOptions' );
if ( $forwardCompatOptions ) {
$this->mUsedOptions = array_keys( $forwardCompatOptions );
}
}
}

View file

@ -1,6 +1,7 @@
<?php
use MediaWiki\Logger\LoggerFactory;
use Wikimedia\Reflection\GhostFieldAccessTrait;
/**
* Output of the PHP parser.
@ -25,6 +26,8 @@ use MediaWiki\Logger\LoggerFactory;
*/
class ParserOutput extends CacheTime {
use GhostFieldAccessTrait;
/**
* Feature flags to indicate to extensions that MediaWiki core supports and
* uses getText() stateless transforms.
@ -1785,6 +1788,7 @@ class ParserOutput extends CacheTime {
*/
protected function initFromJson( array $jsonData ) {
parent::initFromJson( $jsonData );
$this->mUsedOptions = null;
$this->mText = $jsonData['Text'];
$this->mLanguageLinks = $jsonData['LanguageLinks'];
@ -1814,7 +1818,12 @@ class ParserOutput extends CacheTime {
$this->mTimestamp = $jsonData['Timestamp'];
$this->mEnableOOUI = $jsonData['EnableOOUI'];
$this->mIndexPolicy = $jsonData['IndexPolicy'];
$this->mAccessedOptions = $jsonData['AccessedOptions'];
if ( array_key_exists( 'ParseUsedOptions', $jsonData ) ) {
// Forward compatibility
$this->mAccessedOptions = $jsonData['ParseUsedOptions'];
} else {
$this->mAccessedOptions = $jsonData['AccessedOptions'];
}
$this->mExtensionData = $jsonData['ExtensionData'];
$this->mLimitReportData = $jsonData['LimitReportData'];
$this->mLimitReportJSData = $jsonData['LimitReportJSData'];
@ -1876,4 +1885,12 @@ class ParserOutput extends CacheTime {
return $properties;
}
public function __wakeup() {
$forwardOptions = $this->getGhostFieldValue( 'mParseUsedOptions' );
if ( $forwardOptions ) {
$this->mAccessedOptions = $forwardOptions;
$this->mUsedOptions = null;
}
}
}

View file

@ -140,10 +140,10 @@ $wgAutoloadClasses += [
'ParserIntegrationTest' => "$testDir/phpunit/suites/ParserIntegrationTest.php",
'MediaWiki\Tests\Parser\ParserCacheSerializationTestCases' =>
"$testDir/phpunit/includes/parser/ParserCacheSerializationTestCases.php",
'MediaWiki\Tests\Parser\SerializationTestTrait' =>
"$testDir/phpunit/includes/parser/SerializationTestTrait.php",
'MediaWiki\Tests\Parser\SerializationTestUtils' =>
"$testDir/phpunit/includes/parser/SerializationTestUtils.php",
'Wikimedia\Tests\SerializationTestTrait' =>
"$testDir/phpunit/includes/libs/serialization/SerializationTestTrait.php",
'Wikimedia\Tests\SerializationTestUtils' =>
"$testDir/phpunit/includes/libs/serialization/SerializationTestUtils.php",
# tests/phpunit/includes/resourceloader
'ResourceLoaderImageModuleTest' =>
@ -223,6 +223,7 @@ $wgAutoloadClasses += [
# tests/phpunit/unit/includes
'BadFileLookupTest' => "$testDir/phpunit/unit/includes/BadFileLookupTest.php",
'Wikimedia\\Reflection\\GhostFieldTestClass' => "$testDir/phpunit/mocks/GhostFieldTestClass.php",
# tests/phpunit/unit/includes/editpage/Constraint and tests/phpunit/integration/includes/editpage/Constraint
'EditConstraintTestTrait' => "$testDir/phpunit/unit/includes/editpage/Constraint/EditConstraintTestTrait.php",

View file

@ -0,0 +1 @@
{"_type_":"CacheTime","ParseUsedOptions":{"optA":true,"optX":true},"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4"}

View file

@ -0,0 +1 @@
{"_type_":"ParserOutput","Text":"","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":[],"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"ParseUsedOptions":null,"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4"}

View file

@ -0,0 +1 @@
{"_type_":"ParserOutput","Text":"Dummy","LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"OutputHooks":[],"Warnings":[],"Sections":[],"Properties":[],"TOCHTML":"","Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","ExtensionData":[],"LimitReportData":[],"LimitReportJSData":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"ParseUsedOptions":{"optA":true,"optX":true},"CacheExpiry":null,"CacheTime":"","CacheRevisionId":null,"Version":"1.6.4"}

View file

@ -1,6 +1,6 @@
<?php
namespace MediaWiki\Tests\Parser;
namespace Wikimedia\Tests;
use Generator;
use ReflectionClass;

View file

@ -19,9 +19,8 @@
* @ingroup Cache Parser
*/
namespace MediaWiki\Tests\Parser;
namespace Wikimedia\Tests;
use CacheTime;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
@ -167,7 +166,7 @@ class SerializationTestUtils {
*/
public function getSerializedInstances(): array {
$instances = $this->getTestInstances();
return array_map( function ( CacheTime $object ) {
return array_map( function ( $object ) {
return call_user_func( $this->serializer, $object );
}, $instances );
}

View file

@ -5,6 +5,7 @@ namespace MediaWiki\Tests\Parser;
use CacheTime;
use MediaWikiIntegrationTestCase;
use MWTimestamp;
use Wikimedia\Tests\SerializationTestTrait;
/**
* @covers CacheTime

View file

@ -8,6 +8,7 @@ use MediaWikiIntegrationTestCase;
use MWTimestamp;
use ParserOutput;
use Title;
use Wikimedia\Tests\SerializationTestUtils;
/**
* A collection of serialization test cases for parser cache.

View file

@ -1,8 +1,8 @@
<?php
use MediaWiki\Tests\Parser\ParserCacheSerializationTestCases;
use MediaWiki\Tests\Parser\SerializationTestTrait;
use Wikimedia\TestingAccessWrapper;
use Wikimedia\Tests\SerializationTestTrait;
/**
* @covers ParserOutput

View file

@ -9,7 +9,7 @@ use ParserOutput;
require_once __DIR__ . '/../../../../maintenance/Maintenance.php';
require __DIR__ . '/ParserCacheSerializationTestCases.php';
require __DIR__ . '/SerializationTestUtils.php';
require __DIR__ . '/../libs/serialization/SerializationTestUtils.php';
class ValidateParserCacheSerializationTestData extends Maintenance {
@ -55,7 +55,7 @@ class ValidateParserCacheSerializationTestData extends Maintenance {
public function validateSerialization( string $className, array $testInstances ) {
$supportedFormats = ParserCacheSerializationTestCases::getSupportedSerializationFormats( $className );
foreach ( $supportedFormats as $serializationFormat ) {
$serializationUtils = new SerializationTestUtils(
$serializationUtils = new \Wikimedia\Tests\SerializationTestUtils(
$this->getArg( 1 ) ?: __DIR__ . '/../../data/ParserCache',
$testInstances,
$serializationFormat['ext'],

View file

@ -0,0 +1,25 @@
<?php
namespace Wikimedia\Reflection;
/**
* This class used to contain a $privateField, $protectedField and $publicField.
* This is used to test that unserialized instances still have the values of
* these ghost fields and the values can be accessed with GhostFieldAccessTrait.
* @package Wikimedia\Reflection
*/
class GhostFieldTestClass {
use GhostFieldAccessTrait;
public function getPrivateField() {
return $this->getGhostFieldValue( 'privateField' );
}
public function getProtectedField() {
return $this->getGhostFieldValue( 'protectedField' );
}
public function getPublicField() {
return $this->getGhostFieldValue( 'publicField' );
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Wikimedia\Tests\Reflection;
use MediaWikiUnitTestCase;
use Wikimedia\Reflection\GhostFieldTestClass;
use Wikimedia\Tests\SerializationTestUtils;
/**
* @covers \Wikimedia\Reflection\GhostFieldAccessTrait
* @package Wikimedia\Tests\Reflection
*/
class GhostFieldAccessTraitTest extends MediaWikiUnitTestCase {
private $serializationTestUtils;
protected function setUp() : void {
parent::setUp();
// Not using the trait since we only need deserialization tests.
$this->serializationTestUtils = new SerializationTestUtils(
__DIR__ . '/../../data/GhostFieldAccess',
[],
'serialized',
'serialize',
'unserialize'
);
}
private function provideUnserializedInstances( string $testCaseName ) {
$serializationTestUtils = new SerializationTestUtils(
__DIR__ . '/../../data/GhostFieldAccess',
[],
'serialized',
'serialize',
'unserialize'
);
$instances = $serializationTestUtils
->getDeserializedInstancesForTestCase( 'GhostFieldTestClass', $testCaseName );
foreach ( $instances as $instance ) {
yield "{$instance->version}" => [ $instance->object ];
}
}
public function provideUnserializedInstancesWithValues() {
return $this->provideUnserializedInstances( 'withValues' );
}
/**
* @dataProvider provideUnserializedInstancesWithValues
* @param GhostFieldTestClass $instance
*/
public function testUnserializedWithValues( GhostFieldTestClass $instance ) {
$this->assertSame( 'private_value', $instance->getPrivateField() );
$this->assertSame( 'protected_value', $instance->getProtectedField() );
$this->assertSame( 'public_value', $instance->getPublicField() );
}
public function provideUnserializedInstancesWithNulls() {
return $this->provideUnserializedInstances( 'withNulls' );
}
/**
* @dataProvider provideUnserializedInstancesWithNulls
* @param GhostFieldTestClass $instance
*/
public function testUnserializedWithNulls( GhostFieldTestClass $instance ) {
$this->assertNull( $instance->getPrivateField() );
$this->assertNull( $instance->getProtectedField() );
$this->assertNull( $instance->getPublicField() );
}
}