Make Content JsonCodecable
By default this uses the existing ContentHandler::serializeContent() and ::unserializeContent() methods. But in cases where existing PHP serialization preserved fields that ::serializeContent() did not, provide an additional ContentHandler::serializeContentToJsonArray() and ContentHandler::deserializeContentFromJsonArray() methods which can be used. Use these in WikitextContentHandler to preserve the PST flags. Added test cases and a ContentSerializationTestTrait to make it easy to ensure forward- and backward-compatibility in accord with https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility The new JsonCodecable codec will be used to improve PageEditStashContent serialization, which no longer has to PHP-serialize its Content object. New test case added demonstrating compatibility. Bug: T264389 Bug: T161647 Change-Id: I544625136088164561b9169a63aed7450cce82f5 (cherry picked from commit 21576d6c1893079777a1a51d0f81c4941c58e376)
This commit is contained in:
parent
71fea78777
commit
df9756b9a1
21 changed files with 441 additions and 2 deletions
|
|
@ -1142,6 +1142,7 @@ $wgAutoloadLocalClasses = [
|
|||
'MediaWiki\\Content\\Content' => __DIR__ . '/includes/content/Content.php',
|
||||
'MediaWiki\\Content\\ContentHandler' => __DIR__ . '/includes/content/ContentHandler.php',
|
||||
'MediaWiki\\Content\\ContentHandlerFactory' => __DIR__ . '/includes/content/ContentHandlerFactory.php',
|
||||
'MediaWiki\\Content\\ContentJsonCodec' => __DIR__ . '/includes/content/ContentJsonCodec.php',
|
||||
'MediaWiki\\Content\\ContentModelChange' => __DIR__ . '/includes/content/ContentModelChange.php',
|
||||
'MediaWiki\\Content\\CssContent' => __DIR__ . '/includes/content/CssContent.php',
|
||||
'MediaWiki\\Content\\CssContentHandler' => __DIR__ . '/includes/content/CssContentHandler.php',
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ use MediaWiki\Config\Config;
|
|||
use MediaWiki\Config\ConfigFactory;
|
||||
use MediaWiki\Config\ConfigRepository;
|
||||
use MediaWiki\Config\GlobalVarConfig;
|
||||
use MediaWiki\Content\ContentJsonCodec;
|
||||
use MediaWiki\Content\IContentHandlerFactory;
|
||||
use MediaWiki\Content\Renderer\ContentRenderer;
|
||||
use MediaWiki\Content\Transform\ContentTransformer;
|
||||
|
|
@ -966,6 +967,13 @@ class MediaWikiServices extends ServiceContainer {
|
|||
return $this->getService( 'ContentHandlerFactory' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.45
|
||||
*/
|
||||
public function getContentJsonCodec(): ContentJsonCodec {
|
||||
return $this->getService( 'ContentJsonCodec' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.32
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ use MediaWiki\Config\ConfigFactory;
|
|||
use MediaWiki\Config\ConfigRepository;
|
||||
use MediaWiki\Config\ServiceOptions;
|
||||
use MediaWiki\Content\ContentHandlerFactory;
|
||||
use MediaWiki\Content\ContentJsonCodec;
|
||||
use MediaWiki\Content\IContentHandlerFactory;
|
||||
use MediaWiki\Content\Renderer\ContentRenderer;
|
||||
use MediaWiki\Content\Transform\ContentTransformer;
|
||||
|
|
@ -630,6 +631,12 @@ return [
|
|||
);
|
||||
},
|
||||
|
||||
'ContentJsonCodec' => static function ( MediaWikiServices $services ): ContentJsonCodec {
|
||||
return new ContentJsonCodec(
|
||||
$services->getContentHandlerFactory(),
|
||||
);
|
||||
},
|
||||
|
||||
'ContentLanguage' => static function ( MediaWikiServices $services ): Language {
|
||||
return $services->getLanguageFactory()->getLanguage(
|
||||
$services->getMainConfig()->get( MainConfigNames::LanguageCode ) );
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ use MediaWiki\MediaWikiServices;
|
|||
use MediaWiki\Parser\MagicWord;
|
||||
use MediaWiki\Title\Title;
|
||||
use MWException;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Wikimedia\JsonCodec\JsonClassCodec;
|
||||
use Wikimedia\JsonCodec\JsonCodecable;
|
||||
use Wikimedia\JsonCodec\JsonCodecInterface;
|
||||
|
||||
/**
|
||||
* Base implementation for content objects.
|
||||
|
|
@ -42,7 +46,7 @@ use MWException;
|
|||
*
|
||||
* @ingroup Content
|
||||
*/
|
||||
abstract class AbstractContent implements Content {
|
||||
abstract class AbstractContent implements Content, JsonCodecable {
|
||||
/**
|
||||
* Name of the content model this Content object represents.
|
||||
* Use with CONTENT_MODEL_XXX constants
|
||||
|
|
@ -419,6 +423,12 @@ abstract class AbstractContent implements Content {
|
|||
return $result;
|
||||
}
|
||||
|
||||
public static function jsonClassCodec(
|
||||
JsonCodecInterface $codec,
|
||||
ContainerInterface $serviceContainer
|
||||
): JsonClassCodec {
|
||||
return $serviceContainer->get( 'ContentJsonCodec' );
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated class alias since 1.43 */
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ use Action;
|
|||
use DifferenceEngine;
|
||||
use DifferenceEngineSlotDiffRenderer;
|
||||
use InvalidArgumentException;
|
||||
use JsonException;
|
||||
use LogicException;
|
||||
use MediaWiki\CommentStore\CommentStore;
|
||||
use MediaWiki\Content\Renderer\ContentParseParams;
|
||||
|
|
@ -354,6 +355,25 @@ abstract class ContentHandler {
|
|||
*/
|
||||
abstract public function serializeContent( Content $content, $format = null );
|
||||
|
||||
/**
|
||||
* Serializes a Content object of the type supported by this
|
||||
* ContentHandler to an array which is JsonCodecable.
|
||||
*
|
||||
* @since 1.45
|
||||
*
|
||||
* @param Content $content The Content object to serialize
|
||||
*
|
||||
* @return array An array of JsonCodecable content
|
||||
*/
|
||||
public function serializeContentToJsonArray( Content $content ): array {
|
||||
$format = $this->getJsonFormat();
|
||||
$blob = $this->serializeContent( $content, $format );
|
||||
return [
|
||||
'format' => $format,
|
||||
'blob' => $blob,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies transformations on export (returns the blob unchanged by default).
|
||||
* Subclasses may override this to perform transformations such as conversion
|
||||
|
|
@ -381,9 +401,30 @@ abstract class ContentHandler {
|
|||
*
|
||||
* @return Content The Content object created by deserializing $blob
|
||||
* @throws MWContentSerializationException
|
||||
* @see ContentJsonCodec
|
||||
*/
|
||||
abstract public function unserializeContent( $blob, $format = null );
|
||||
|
||||
/**
|
||||
* Deserializes a Content object of the type supported by this
|
||||
* ContentHandler from a JsonCodecable array.
|
||||
*
|
||||
* @since 1.45
|
||||
*
|
||||
* @param array $json Serialized form of the content
|
||||
*
|
||||
* @return Content The Content object created by deserializing $blob
|
||||
* @throws JsonException
|
||||
* @see ContentJsonCodec
|
||||
*/
|
||||
public function deserializeContentFromJsonArray( array $json ): Content {
|
||||
try {
|
||||
return $this->unserializeContent( $json['blob'], $json['format'] );
|
||||
} catch ( MWContentSerializationException $e ) {
|
||||
throw new JsonException( $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply import transformation (by default, returns $blob unchanged).
|
||||
* This gives subclasses an opportunity to transform data blobs on import.
|
||||
|
|
@ -491,6 +532,16 @@ abstract class ContentHandler {
|
|||
return $this->mSupportedFormats[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow ContentHandler to chose a non-default format for JSON
|
||||
* serialization.
|
||||
*
|
||||
* In most cases will return the same as `::getDefaultFormat()`.
|
||||
*/
|
||||
public function getJsonFormat(): string {
|
||||
return $this->getDefaultFormat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if $format is a serialization format supported by this
|
||||
* ContentHandler, and false otherwise.
|
||||
|
|
|
|||
69
includes/content/ContentJsonCodec.php
Normal file
69
includes/content/ContentJsonCodec.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
declare( strict_types = 1 );
|
||||
/**
|
||||
* 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 MediaWiki\Content;
|
||||
|
||||
use Wikimedia\JsonCodec\JsonClassCodec;
|
||||
|
||||
/**
|
||||
* ContentJsonCodec handles serialization of Content objects to/from
|
||||
* JSON using methods of the appropriate ContentHandler.
|
||||
*
|
||||
* @template-implements JsonClassCodec<Content>
|
||||
* @internal
|
||||
*/
|
||||
class ContentJsonCodec implements JsonClassCodec {
|
||||
|
||||
public function __construct(
|
||||
private IContentHandlerFactory $contentHandlerFactory
|
||||
) {
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function toJsonArray( $content ): array {
|
||||
'@phan-var Content $content'; /** @var Content $content */
|
||||
// To serialize content we need a handler.
|
||||
$model = $content->getModel();
|
||||
$handler = $this->contentHandlerFactory->getContentHandler(
|
||||
$model
|
||||
);
|
||||
return [ 'model' => $model ] +
|
||||
$handler->serializeContentToJsonArray( $content );
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function newFromJsonArray( string $className, array $json ) {
|
||||
// To deserialize content we need a handler.
|
||||
$model = $json['model'];
|
||||
$handler = $this->contentHandlerFactory->getContentHandler(
|
||||
$model
|
||||
);
|
||||
$content = $handler->deserializeContentFromJsonArray( $json );
|
||||
// phan's support for generics is broken :(
|
||||
// @phan-suppress-next-line PhanTypeMismatchReturn
|
||||
return $content;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function jsonClassHintFor( string $className, string $keyName ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -424,6 +424,20 @@ class WikitextContentHandler extends TextContentHandler {
|
|||
$parserOutput->setOutputFlag( ParserOutputFlags::USER_SIGNATURE );
|
||||
}
|
||||
}
|
||||
|
||||
public function serializeContentToJsonArray( Content $content ): array {
|
||||
'@phan-var WikitextContent $content'; /** @var WikitextContent $content */
|
||||
return parent::serializeContentToJsonArray( $content ) + [
|
||||
'pstFlags' => $content->getPreSaveTransformFlags(),
|
||||
];
|
||||
}
|
||||
|
||||
public function deserializeContentFromJsonArray( array $json ): WikitextContent {
|
||||
$content = parent::deserializeContentFromJsonArray( $json );
|
||||
'@phan-var WikitextContent $content'; /** @var WikitextContent $content */
|
||||
$content->setPreSaveTransformFlags( $json['pstFlags'] ?? [] );
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated class alias since 1.43 */
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ $wgAutoloadClasses += [
|
|||
'LoggedServiceOptions' => "$testDir/phpunit/includes/config/LoggedServiceOptions.php",
|
||||
|
||||
# tests/phpunit/includes/content
|
||||
'MediaWiki\\Tests\\Content\\CssContentTest' => "$testDir/phpunit/includes/content/CssContentTest.php",
|
||||
'DummyContentHandlerForTesting' =>
|
||||
"$testDir/phpunit/mocks/content/DummyContentHandlerForTesting.php",
|
||||
'DummyContentForTesting' => "$testDir/phpunit/mocks/content/DummyContentForTesting.php",
|
||||
|
|
@ -127,10 +128,13 @@ $wgAutoloadClasses += [
|
|||
'DummyNonTextContent' => "$testDir/phpunit/mocks/content/DummyNonTextContent.php",
|
||||
'DummySerializeErrorContentHandler' =>
|
||||
"$testDir/phpunit/mocks/content/DummySerializeErrorContentHandler.php",
|
||||
'MediaWiki\\Tests\\Content\\JavaScriptContentTest' => "$testDir/phpunit/includes/content/JavaScriptContentTest.php",
|
||||
'TextContentTest' => "$testDir/phpunit/includes/content/TextContentTest.php",
|
||||
'MediaWiki\\Tests\\Content\\TextContentTest' => "$testDir/phpunit/includes/content/TextContentTest.php",
|
||||
'TextContentHandlerIntegrationTest' => "$testDir/phpunit/includes/content/TextContentHandlerIntegrationTest.php",
|
||||
'WikitextContentTest' => "$testDir/phpunit/includes/content/WikitextContentTest.php",
|
||||
'MediaWiki\\Tests\\Content\\WikitextContentTest' => "$testDir/phpunit/includes/content/WikitextContentTest.php",
|
||||
'JavaScriptContentHandlerTest' => "$testDir/phpunit/includes/content/JavaScriptContentHandlerTest.php",
|
||||
'MediaWiki\\Tests\\Content\\ContentSerializationTestTrait' => "$testDir/phpunit/includes/content/ContentSerializationTestTrait.php",
|
||||
|
||||
# tests/phpunit/includes/db
|
||||
'DatabaseTestHelper' => "$testDir/phpunit/includes/db/DatabaseTestHelper.php",
|
||||
|
|
|
|||
1
tests/phpunit/data/Content/1.45-CssContent-basic.json
Normal file
1
tests/phpunit/data/Content/1.45-CssContent-basic.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"model":"css","format":"text/css","blob":"/* hello */","_type_":"MediaWiki\\Content\\CssContent","_complex_":true}
|
||||
1
tests/phpunit/data/Content/1.45-CssContent-redirect.json
Normal file
1
tests/phpunit/data/Content/1.45-CssContent-redirect.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"model":"css","format":"text/css","blob":"/* #REDIRECT */@import url(//example.org/w/index.php?title=MediaWiki:MonoBook.css&action=raw&ctype=text/css);","_type_":"MediaWiki\\Content\\CssContent","_complex_":true}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"model":"javascript","format":"text/javascript","blob":"/* hello */","_type_":"MediaWiki\\Content\\JavaScriptContent","_complex_":true}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"model":"javascript","format":"text/javascript","blob":"/* #REDIRECT */mw.loader.load(\"//example.org/w/index.php?title=MediaWiki:MonoBook.js&action=raw&ctype=text/javascript\");","_type_":"MediaWiki\\Content\\JavaScriptContent","_complex_":true}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"model":"wikitext","format":"text/x-wiki","blob":"hello","pstFlags":[],"_type_":"MediaWiki\\Content\\WikitextContent","_complex_":true}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"model":"wikitext","format":"text/x-wiki","blob":"with PST flags","pstFlags":["show-toc","vary-revision"],"_type_":"MediaWiki\\Content\\WikitextContent","_complex_":true}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"pstContent":{"model":"wikitext","format":"text/x-wiki","blob":"hello","_type_":"MediaWiki\\Content\\WikitextContent","_complex_":true},"output":{"Text":null,"LanguageLinks":[],"Categories":[],"Indicators":[],"TitleText":"","Links":[],"LinksSpecial":[],"Templates":[],"TemplateIds":[],"Images":[],"FileSearchOptions":[],"ExternalLinks":[],"InterwikiLinks":[],"ExistenceLinks":[],"NewSection":false,"HideNewSection":false,"NoGallery":false,"HeadItems":[],"Modules":[],"ModuleStyles":[],"JsConfigVars":[],"Warnings":[],"WarningMsgs":[],"Sections":[],"Properties":[],"Timestamp":null,"EnableOOUI":false,"IndexPolicy":"","ExtensionData":{"test1":"test2"},"LimitReportData":[],"LimitReportJSData":[],"CacheMessage":"","TimeProfile":[],"ParseStartTime":[],"PreventClickjacking":false,"ExtraScriptSrcs":[],"ExtraDefaultSrcs":[],"ExtraStyleSrcs":[],"Flags":[],"SpeculativeRevId":null,"SpeculativePageIdUsed":null,"RevisionTimestampUsed":null,"RevisionUsedSha1Base36":null,"WrapperDivClasses":[],"ParseUsedOptions":[],"CacheExpiry":null,"CacheTime":"20250701173710","CacheRevisionId":null,"_type_":"MediaWiki\\Parser\\ParserOutput","_complex_":true},"timestamp":"20250701173710","edits":42,"_type_":"MediaWiki\\Storage\\PageEditStashContents","_complex_":true}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace MediaWiki\Tests\Content;
|
||||
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use Wikimedia\Tests\SerializationTestTrait;
|
||||
|
||||
trait ContentSerializationTestTrait {
|
||||
use SerializationTestTrait;
|
||||
|
||||
public static function getSerializedDataPath(): string {
|
||||
return __DIR__ . '/../../data/Content';
|
||||
}
|
||||
|
||||
public static function getSupportedSerializationFormats(): array {
|
||||
$jsonCodec = MediaWikiServices::getInstance()->getJsonCodec();
|
||||
return [ [
|
||||
'ext' => 'json',
|
||||
'serializer' => static function ( $obj ) use ( $jsonCodec ) {
|
||||
return $jsonCodec->serialize( $obj );
|
||||
},
|
||||
'deserializer' => static function ( $data ) use ( $jsonCodec ) {
|
||||
return $jsonCodec->deserialize( $data );
|
||||
},
|
||||
] ];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace MediaWiki\Tests\Content;
|
||||
|
||||
use MediaWiki\Content\Content;
|
||||
use MediaWiki\Content\CssContent;
|
||||
|
|
@ -12,6 +15,7 @@ use MediaWiki\MainConfigNames;
|
|||
* @covers \MediaWiki\Content\CssContent
|
||||
*/
|
||||
class CssContentTest extends TextContentTest {
|
||||
use ContentSerializationTestTrait;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
|
@ -115,4 +119,36 @@ class CssContentTest extends TextContentTest {
|
|||
public function testEquals( Content $a, ?Content $b = null, $equal = false ) {
|
||||
$this->assertEquals( $equal, $a->equals( $b ) );
|
||||
}
|
||||
|
||||
public static function getClassToTest(): string {
|
||||
return CssContent::class;
|
||||
}
|
||||
|
||||
public static function getTestInstancesAndAssertions(): array {
|
||||
$redirects = self::provideGetRedirectTarget();
|
||||
[ $redirectTitle, $redirectBlob ] = $redirects[0];
|
||||
return [
|
||||
'basic' => [
|
||||
'instance' => new CssContent( '/* hello */' ),
|
||||
'assertions' => static function ( $testCase, $obj ) {
|
||||
$testCase->assertInstanceof( CssContent::class, $obj );
|
||||
$testCase->assertSame( '/* hello */', $obj->getText() );
|
||||
$testCase->assertNull( $obj->getRedirectTarget() );
|
||||
},
|
||||
],
|
||||
'redirect' => [
|
||||
'instance' => new CssContent( $redirectBlob ),
|
||||
'assertions' => static function ( $testCase, $obj ) use ( $redirectTitle, $redirectBlob ) {
|
||||
$testCase->overrideConfigValues( [
|
||||
MainConfigNames::Server => '//example.org',
|
||||
MainConfigNames::ScriptPath => '/w',
|
||||
MainConfigNames::Script => '/w/index.php',
|
||||
] );
|
||||
$testCase->assertInstanceof( CssContent::class, $obj );
|
||||
$testCase->assertSame( $redirectBlob, $obj->getText() );
|
||||
$testCase->assertSame( $redirectTitle, $obj->getRedirectTarget()->getPrefixedText() );
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
<?php
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace MediaWiki\Tests\Content;
|
||||
|
||||
use JavaScriptContentHandlerTest;
|
||||
use MediaWiki\Content\CssContent;
|
||||
use MediaWiki\Content\JavaScriptContent;
|
||||
use MediaWiki\MainConfigNames;
|
||||
|
|
@ -13,6 +17,7 @@ use MediaWiki\Title\Title;
|
|||
* @covers \MediaWiki\Content\JavaScriptContent
|
||||
*/
|
||||
class JavaScriptContentTest extends TextContentTest {
|
||||
use ContentSerializationTestTrait;
|
||||
|
||||
public function newContent( $text ) {
|
||||
return new JavaScriptContent( $text );
|
||||
|
|
@ -213,4 +218,37 @@ class JavaScriptContentTest extends TextContentTest {
|
|||
'/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=MediaWiki:MonoBook.js\u0026action=raw\u0026ctype=text/javascript");'
|
||||
];
|
||||
}
|
||||
|
||||
public static function getClassToTest(): string {
|
||||
return JavaScriptContent::class;
|
||||
}
|
||||
|
||||
public static function getTestInstancesAndAssertions(): array {
|
||||
$redirects = JavaScriptContentHandlerTest::provideMakeRedirectContent();
|
||||
[ $redirectTitle, $redirectBlob ] = $redirects['MediaWiki namespace page'];
|
||||
return [
|
||||
'basic' => [
|
||||
'instance' => new JavaScriptContent( '/* hello */' ),
|
||||
'assertions' => static function ( $testCase, $obj ) {
|
||||
$testCase->assertInstanceof( JavaScriptContent::class, $obj );
|
||||
$testCase->assertSame( '/* hello */', $obj->getText() );
|
||||
$testCase->assertNull( $obj->getRedirectTarget() );
|
||||
},
|
||||
],
|
||||
'redirect' => [
|
||||
'instance' => new JavaScriptContent( $redirectBlob ),
|
||||
'assertions' => static function ( $testCase, $obj ) use ( $redirectTitle, $redirectBlob ) {
|
||||
$testCase->overrideConfigValues( [
|
||||
MainConfigNames::Server => '//example.org',
|
||||
MainConfigNames::ScriptPath => '/w',
|
||||
MainConfigNames::Script => '/w/index.php',
|
||||
MainConfigNames::ResourceBasePath => '/w',
|
||||
] );
|
||||
$testCase->assertInstanceof( JavaScriptContent::class, $obj );
|
||||
$testCase->assertSame( $redirectBlob, $obj->getText() );
|
||||
$testCase->assertSame( $redirectTitle, $obj->getRedirectTarget()->getPrefixedText() );
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace MediaWiki\Tests\Content;
|
||||
|
||||
use MediaWiki\Content\Content;
|
||||
use MediaWiki\Content\JavaScriptContent;
|
||||
|
|
@ -8,6 +11,7 @@ use MediaWiki\Context\RequestContext;
|
|||
use MediaWiki\MainConfigNames;
|
||||
use MediaWiki\Title\Title;
|
||||
use MediaWiki\User\User;
|
||||
use MediaWikiLangTestCase;
|
||||
|
||||
/**
|
||||
* Needs database to do link updates.
|
||||
|
|
@ -290,3 +294,4 @@ class TextContentTest extends MediaWikiLangTestCase {
|
|||
}
|
||||
|
||||
}
|
||||
class_alias( TextContentTest::class, 'TextContentTest' );
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Tests\Content;
|
||||
|
||||
use MediaWiki\Content\JavaScriptContent;
|
||||
use MediaWiki\Content\TextContent;
|
||||
use MediaWiki\Content\WikitextContent;
|
||||
use MediaWiki\Deferred\LinksUpdate\LinksDeletionUpdate;
|
||||
use MediaWiki\Parser\Parser;
|
||||
use MediaWiki\Parser\ParserOptions;
|
||||
use MediaWiki\Parser\ParserOutputFlags;
|
||||
use MediaWiki\Title\Title;
|
||||
|
||||
/**
|
||||
|
|
@ -16,6 +19,8 @@ use MediaWiki\Title\Title;
|
|||
* @covers \MediaWiki\Content\WikitextContent
|
||||
*/
|
||||
class WikitextContentTest extends TextContentTest {
|
||||
use ContentSerializationTestTrait;
|
||||
|
||||
public const SECTIONS = "Intro
|
||||
|
||||
== stuff ==
|
||||
|
|
@ -348,4 +353,38 @@ just a test"
|
|||
// @todo more...?
|
||||
];
|
||||
}
|
||||
|
||||
public static function getClassToTest(): string {
|
||||
return WikitextContent::class;
|
||||
}
|
||||
|
||||
public static function getTestInstancesAndAssertions(): array {
|
||||
// Note that WikitextContent::{get,set}PreSaveTransformFlags()
|
||||
// is preserved over JSON serialization.
|
||||
$pstFlags = [
|
||||
ParserOutputFlags::SHOW_TOC,
|
||||
ParserOutputFlags::VARY_REVISION,
|
||||
];
|
||||
$withPstFlags = new WikitextContent( 'with PST flags' );
|
||||
$withPstFlags->setPreSaveTransformFlags( $pstFlags );
|
||||
|
||||
return [
|
||||
'basic' => [
|
||||
'instance' => new WikitextContent( 'hello' ),
|
||||
'assertions' => static function ( $testCase, $obj ) {
|
||||
$testCase->assertInstanceof( WikitextContent::class, $obj );
|
||||
$testCase->assertSame( 'hello', $obj->getText() );
|
||||
$testCase->assertArrayEquals( [], $obj->getPreSaveTransformFlags() );
|
||||
},
|
||||
],
|
||||
'withPstFlags' => [
|
||||
'instance' => $withPstFlags,
|
||||
'assertions' => static function ( $testCase, $obj ) use ( $pstFlags ) {
|
||||
$testCase->assertInstanceof( WikitextContent::class, $obj );
|
||||
$testCase->assertSame( 'with PST flags', $obj->getText() );
|
||||
$testCase->assertArrayEquals( $pstFlags, $obj->getPreSaveTransformFlags() );
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
122
tests/phpunit/includes/content/validateContentTestData.php
Normal file
122
tests/phpunit/includes/content/validateContentTestData.php
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Tests\Content;
|
||||
|
||||
use MediaWiki\Logger\ConsoleLogger;
|
||||
use MediaWiki\Maintenance\Maintenance;
|
||||
use Wikimedia\Tests\SerializationTestUtils;
|
||||
|
||||
define( 'MW_AUTOLOAD_TEST_CLASSES', true );
|
||||
define( 'MW_PHPUNIT_TEST', true );
|
||||
|
||||
require_once __DIR__ . '/../../../../maintenance/Maintenance.php';
|
||||
|
||||
// phpcs:disable MediaWiki.Files.ClassMatchesFilename.WrongCase
|
||||
class ValidateContentTestData extends Maintenance {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
|
||||
$this->addArg(
|
||||
'path',
|
||||
'Path of serialization files.',
|
||||
false
|
||||
);
|
||||
$this->addOption( 'create', 'Create missing serialization' );
|
||||
$this->addOption( 'update', 'Update mismatching serialization files' );
|
||||
$this->addOption( 'version', 'Specify version for which to check serialization. '
|
||||
. 'Also determines which files may be created or updated if '
|
||||
. 'the respective options are set.'
|
||||
. 'Unserialization is always checked against all versions. ', false, true );
|
||||
}
|
||||
|
||||
public function execute() {
|
||||
$tests = [
|
||||
WikitextContentTest::class,
|
||||
JavaScriptContentTest::class,
|
||||
CssContentTest::class,
|
||||
];
|
||||
foreach ( $tests as $testClass ) {
|
||||
$objClass = $testClass::getClassToTest();
|
||||
$this->validateSerialization(
|
||||
$objClass,
|
||||
$testClass::getSerializedDataPath(),
|
||||
$testClass::getSupportedSerializationFormats(),
|
||||
array_map( static function ( $testCase ) {
|
||||
return $testCase['instance'];
|
||||
}, $testClass::getTestInstancesAndAssertions() )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that objects will serialize into the form expected for the given version.
|
||||
* If the respective options are set in the constructor, this will create missing files or
|
||||
* update mismatching files.
|
||||
*
|
||||
* @param string $className
|
||||
* @param string $defaultDirectory
|
||||
* @param array $supportedFormats
|
||||
* @param array $testInstances
|
||||
*/
|
||||
public function validateSerialization(
|
||||
string $className,
|
||||
string $defaultDirectory,
|
||||
array $supportedFormats,
|
||||
array $testInstances
|
||||
) {
|
||||
$ok = true;
|
||||
foreach ( $supportedFormats as $serializationFormat ) {
|
||||
$serializationUtils = new SerializationTestUtils(
|
||||
$this->getArg( 1 ) ?: $defaultDirectory,
|
||||
$testInstances,
|
||||
$serializationFormat['ext'],
|
||||
$serializationFormat['serializer'],
|
||||
$serializationFormat['deserializer']
|
||||
);
|
||||
$serializationUtils->setLogger( new ConsoleLogger( 'validator' ) );
|
||||
foreach ( $serializationUtils->getSerializedInstances() as $testCaseName => $currentSerialized ) {
|
||||
$expected = $serializationUtils
|
||||
->getStoredSerializedInstance( $className, $testCaseName, $this->getOption( 'version' ) );
|
||||
$ok = $this->validateSerializationData( $currentSerialized, $expected ) && $ok;
|
||||
}
|
||||
}
|
||||
if ( !$ok ) {
|
||||
$this->output( "\n\n" );
|
||||
$this->fatalError( "Serialization data mismatch! "
|
||||
. "If this was expected, rerun the script with the --update option "
|
||||
. "to update the expected serialization. WARNING: make sure "
|
||||
. "a forward compatible version of the code is live before deploying a "
|
||||
. "serialization change!\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function validateSerializationData( string $data, \stdClass $fileInfo ): bool {
|
||||
if ( !$fileInfo->data ) {
|
||||
if ( $this->hasOption( 'create' ) ) {
|
||||
$this->output( 'Creating file: ' . $fileInfo->path . "\n" );
|
||||
file_put_contents( $fileInfo->path, $data );
|
||||
} else {
|
||||
$this->fatalError( "File not found: {$fileInfo->path}. "
|
||||
. "Rerun the script with the --create option set to create it."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if ( $data !== $fileInfo->data ) {
|
||||
if ( $this->hasOption( 'update' ) ) {
|
||||
$this->output( 'Data mismatch, updating file: ' . $fileInfo->currentVersionPath . "\n" );
|
||||
file_put_contents( $fileInfo->currentVersionPath, $data );
|
||||
} else {
|
||||
$this->output( 'Serialization MISMATCH: ' . $fileInfo->path . "\n" );
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$this->output( "Serialization OK: " . $fileInfo->path . "\n" );
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return ValidateContentTestData::class;
|
||||
Loading…
Reference in a new issue