Rename JsonUnserial… to JsonDeserial…
[mediawiki.git] / tests / phpunit / includes / parser / ParserCacheTest.php
blob0299f57b22ebd0aceefe07ced2d57c08a4339b59
1 <?php
3 namespace MediaWiki\Tests\Parser;
5 use BagOStuff;
6 use CacheTime;
7 use EmptyBagOStuff;
8 use HashBagOStuff;
9 use InvalidArgumentException;
10 use MediaWiki\HookContainer\HookContainer;
11 use MediaWiki\Json\JsonCodec;
12 use MediaWiki\Page\PageRecord;
13 use MediaWiki\Page\PageStoreRecord;
14 use MediaWiki\Page\WikiPageFactory;
15 use MediaWiki\Parser\ParserCacheFilter;
16 use MediaWiki\Parser\ParserOutput;
17 use MediaWiki\Tests\Json\JsonDeserializableSuperClass;
18 use MediaWiki\Title\Title;
19 use MediaWiki\Title\TitleFactory;
20 use MediaWiki\User\User;
21 use MediaWiki\Utils\MWTimestamp;
22 use MediaWikiIntegrationTestCase;
23 use ParserCache;
24 use ParserOptions;
25 use Psr\Log\LoggerInterface;
26 use Psr\Log\LogLevel;
27 use Psr\Log\NullLogger;
28 use TestLogger;
29 use Wikimedia\Stats\StatsFactory;
30 use Wikimedia\TestingAccessWrapper;
31 use Wikimedia\UUID\GlobalIdGenerator;
32 use WikiPage;
34 /**
35 * @covers \ParserCache
37 class ParserCacheTest extends MediaWikiIntegrationTestCase {
39 /** @var int */
40 private $time;
42 /** @var string */
43 private $cacheTime;
45 /** @var PageRecord */
46 private $page;
48 protected function setUp(): void {
49 parent::setUp();
50 $this->time = time();
51 $this->cacheTime = MWTimestamp::convert( TS_MW, $this->time + 1 );
52 $this->page = $this->createPageRecord();
54 MWTimestamp::setFakeTime( $this->time );
57 /**
58 * @param array $overrides
59 * @return PageRecord
61 private function createPageRecord( array $overrides = [] ): PageRecord {
62 return new PageStoreRecord( (object)array_merge( [
63 'page_id' => 42,
64 'page_namespace' => NS_MAIN,
65 'page_title' => 'Testing_Testing',
66 'page_latest' => 24,
67 'page_is_new' => false,
68 'page_is_redirect' => false,
69 'page_touched' => $this->time,
70 'page_lang' => 'qqx',
71 ], $overrides ), PageRecord::LOCAL );
74 /**
75 * @param HookContainer|null $hookContainer
76 * @param BagOStuff|null $storage
77 * @param LoggerInterface|null $logger
78 * @param WikiPageFactory|null $wikiPageFactory
79 * @return ParserCache
81 private function createParserCache(
82 HookContainer $hookContainer = null,
83 BagOStuff $storage = null,
84 LoggerInterface $logger = null,
85 WikiPageFactory $wikiPageFactory = null
86 ): ParserCache {
87 if ( !$wikiPageFactory ) {
88 $wikiPageMock = $this->createMock( WikiPage::class );
89 $wikiPageMock->method( 'getContentModel' )->willReturn( CONTENT_MODEL_WIKITEXT );
90 $wikiPageFactory = $this->createMock( WikiPageFactory::class );
91 $wikiPageFactory->method( 'newFromTitle' )->willReturn( $wikiPageMock );
93 $globalIdGenerator = $this->createMock( GlobalIdGenerator::class );
94 $globalIdGenerator->method( 'newUUIDv1' )->willReturn( 'uuid-uuid' );
95 return new ParserCache(
96 'test',
97 $storage ?: new HashBagOStuff(),
98 '19900220000000',
99 $hookContainer ?: $this->createHookContainer( [] ),
100 new JsonCodec(),
101 StatsFactory::newNull(),
102 $logger ?: new NullLogger(),
103 $this->createMock( TitleFactory::class ),
104 $wikiPageFactory,
105 $globalIdGenerator
110 * @return array
112 private function getDummyUsedOptions(): array {
113 return array_slice(
114 ParserOptions::allCacheVaryingOptions(),
121 * @return ParserOutput
123 private function createDummyParserOutput(): ParserOutput {
124 $parserOutput = new ParserOutput();
125 $parserOutput->setRawText( 'TEST' );
126 foreach ( $this->getDummyUsedOptions() as $option ) {
127 $parserOutput->recordOption( $option );
129 $parserOutput->updateCacheExpiry( 4242 );
130 $parserOutput->setRenderId( 'dummy-render-id' );
131 $parserOutput->setCacheRevisionId( 0 );
132 // ParserOutput::getCacheTime() also sets it as a side effect
133 $parserOutput->setRevisionTimestamp( $parserOutput->getCacheTime() );
134 return $parserOutput;
138 * @covers \ParserCache::getMetadata
140 public function testGetMetadataMissing() {
141 $cache = $this->createParserCache();
142 $metadataFromCache = $cache->getMetadata( $this->page, ParserCache::USE_CURRENT_ONLY );
143 $this->assertNull( $metadataFromCache );
147 * @covers \ParserCache::getMetadata
149 public function testGetMetadataAllGood() {
150 $cache = $this->createParserCache();
151 $parserOutput = $this->createDummyParserOutput();
153 $cache->save( $parserOutput, $this->page, ParserOptions::newFromAnon(), $this->cacheTime );
155 $metadataFromCache = $cache->getMetadata( $this->page, ParserCache::USE_CURRENT_ONLY );
156 $this->assertNotNull( $metadataFromCache );
157 $this->assertSame( $this->getDummyUsedOptions(), $metadataFromCache->getUsedOptions() );
158 $this->assertSame( 4242, $metadataFromCache->getCacheExpiry() );
159 $this->assertSame( $this->page->getLatest(), $metadataFromCache->getCacheRevisionId() );
160 $this->assertSame( $this->cacheTime, $metadataFromCache->getCacheTime() );
164 * @covers \ParserCache::getMetadata
166 public function testGetMetadataExpired() {
167 $cache = $this->createParserCache();
168 $parserOutput = $this->createDummyParserOutput();
169 $cache->save( $parserOutput, $this->page, ParserOptions::newFromAnon(), $this->cacheTime );
171 $this->page = $this->createPageRecord( [ 'page_touched' => $this->time + 10000 ] );
172 $this->assertNull( $cache->getMetadata( $this->page, ParserCache::USE_CURRENT_ONLY ) );
173 $metadataFromCache = $cache->getMetadata( $this->page, ParserCache::USE_EXPIRED );
174 $this->assertNotNull( $metadataFromCache );
175 $this->assertSame( $this->getDummyUsedOptions(), $metadataFromCache->getUsedOptions() );
176 $this->assertSame( 4242, $metadataFromCache->getCacheExpiry() );
177 $this->assertSame( $this->page->getLatest(), $metadataFromCache->getCacheRevisionId() );
178 $this->assertSame( $this->cacheTime, $metadataFromCache->getCacheTime() );
182 * @covers \ParserCache::getMetadata
184 public function testGetMetadataOutdated() {
185 $cache = $this->createParserCache();
186 $parserOutput = $this->createDummyParserOutput();
187 $cache->save( $parserOutput, $this->page, ParserOptions::newFromAnon(), $this->cacheTime );
189 $this->page = $this->createPageRecord( [ 'page_latest' => $this->page->getLatest() + 1 ] );
190 $this->assertNull( $cache->getMetadata( $this->page, ParserCache::USE_CURRENT_ONLY ) );
191 $this->assertNull( $cache->getMetadata( $this->page, ParserCache::USE_EXPIRED ) );
192 $metadataFromCache = $cache->getMetadata( $this->page, ParserCache::USE_OUTDATED );
193 $this->assertSame( $this->getDummyUsedOptions(), $metadataFromCache->getUsedOptions() );
194 $this->assertSame( 4242, $metadataFromCache->getCacheExpiry() );
195 $this->assertNotSame( $this->page->getLatest(), $metadataFromCache->getCacheRevisionId() );
196 $this->assertSame( $this->cacheTime, $metadataFromCache->getCacheTime() );
200 * @covers \ParserCache::makeParserOutputKey
202 public function testMakeParserOutputKey() {
203 $cache = $this->createParserCache();
205 $options1 = ParserOptions::newFromAnon();
206 $options1->setOption( $this->getDummyUsedOptions()[0], 'value1' );
207 $key1 = $cache->makeParserOutputKey( $this->page, $options1, $this->getDummyUsedOptions() );
208 $this->assertNotNull( $key1 );
210 $options2 = ParserOptions::newFromAnon();
211 $options2->setOption( $this->getDummyUsedOptions()[0], 'value2' );
212 $key2 = $cache->makeParserOutputKey( $this->page, $options2, $this->getDummyUsedOptions() );
213 $this->assertNotNull( $key2 );
214 $this->assertNotSame( $key1, $key2 );
218 * Test that fetching without storing first returns false.
219 * @covers \ParserCache::get
221 public function testGetEmpty() {
222 $cache = $this->createParserCache();
223 $options = ParserOptions::newFromAnon();
225 $this->assertFalse( $cache->get( $this->page, $options ) );
229 * Test that fetching with the same options return the saved value.
230 * @covers \ParserCache::get
231 * @covers \ParserCache::save
233 public function testSaveGetSameOptions() {
234 $cache = $this->createParserCache();
235 $parserOutput = new ParserOutput( 'TEST_TEXT' );
237 $options1 = ParserOptions::newFromAnon();
238 $options1->setOption( $this->getDummyUsedOptions()[0], 'value1' );
239 $cache->save( $parserOutput, $this->page, $options1, $this->cacheTime );
241 $savedOutput = $cache->get( $this->page, $options1 );
242 $this->assertInstanceOf( ParserOutput::class, $savedOutput );
243 // ParserCache adds a comment to the HTML, so check if the result starts with page content.
244 $this->assertStringStartsWith( 'TEST_TEXT', $savedOutput->getText() );
245 $this->assertSame( $this->cacheTime, $savedOutput->getCacheTime() );
246 $this->assertSame( $this->page->getLatest(), $savedOutput->getCacheRevisionId() );
250 * Test that fetching with different unused option returns a value.
251 * @covers \ParserCache::get
252 * @covers \ParserCache::save
254 public function testSaveGetDifferentUnusedOption() {
255 $cache = $this->createParserCache();
256 $optionName = $this->getDummyUsedOptions()[0];
257 $parserOutput = new ParserOutput( 'TEST_TEXT' );
259 $options1 = ParserOptions::newFromAnon();
260 $options1->setOption( $optionName, 'value1' );
261 $cache->save( $parserOutput, $this->page, $options1, $this->cacheTime );
263 $options2 = ParserOptions::newFromAnon();
264 $options2->setOption( $optionName, 'value2' );
265 $savedOutput = $cache->get( $this->page, $options2 );
266 $this->assertInstanceOf( ParserOutput::class, $savedOutput );
267 // ParserCache adds a comment to the HTML, so check if the result starts with page content.
268 $this->assertStringStartsWith( 'TEST_TEXT', $savedOutput->getText() );
269 $this->assertSame( $this->cacheTime, $savedOutput->getCacheTime() );
270 $this->assertSame( $this->page->getLatest(), $savedOutput->getCacheRevisionId() );
274 * Test that non-cacheable output is not stored
275 * @covers \ParserCache::save
276 * @covers \ParserCache::get
278 public function testDoesNotStoreNonCacheable() {
279 $cache = $this->createParserCache();
280 $parserOutput = new ParserOutput( 'TEST_TEXT' );
281 $parserOutput->updateCacheExpiry( 0 );
283 $options1 = ParserOptions::newFromAnon();
284 $cache->save( $parserOutput, $this->page, $options1, $this->cacheTime );
286 $this->assertFalse( $cache->get( $this->page, $options1 ) );
287 $this->assertFalse( $cache->get( $this->page, $options1, true ) );
288 $this->assertFalse( $cache->getDirty( $this->page, $options1 ) );
292 * Test that ParserCacheFilter can be used to prevent content from being cached
293 * @covers \ParserCache::save
294 * @covers \ParserCache::get
296 public function testDoesNotStoreFiltered() {
297 $cache = $this->createParserCache();
298 $parserOutput = new ParserOutput( 'TEST_TEXT' );
299 $parserOutput->resetParseStartTime();
300 $parserOutput->recordTimeProfile();
302 // Only cache output that took at least 100 seconds of CPU to generate
303 $cache->setFilter( new ParserCacheFilter( [
304 'default' => [ 'minCpuTime' => 100 ]
305 ] ) );
307 $options1 = ParserOptions::newFromAnon();
308 $cache->save( $parserOutput, $this->page, $options1, $this->cacheTime );
310 $this->assertFalse( $cache->get( $this->page, $options1 ) );
311 $this->assertFalse( $cache->get( $this->page, $options1, true ) );
312 $this->assertFalse( $cache->getDirty( $this->page, $options1 ) );
316 * Test that ParserOptions::isSafeToCache is respected on save
317 * @covers \ParserCache::save
319 public function testDoesNotStoreNotSafeToCacheAndUsed() {
320 $cache = $this->createParserCache();
321 $parserOutput = new ParserOutput( 'TEST_TEXT' );
322 $parserOutput->recordOption( 'wrapclass' );
324 $options = ParserOptions::newFromAnon();
325 $options->setOption( 'wrapclass', 'wrapwrap' );
327 $cache->save( $parserOutput, $this->page, $options, $this->cacheTime );
329 $this->assertFalse( $cache->get( $this->page, $options ) );
330 $this->assertFalse( $cache->get( $this->page, $options, true ) );
331 $this->assertFalse( $cache->getDirty( $this->page, $options ) );
335 * Test that ParserOptions::isSafeToCache is respected on get
336 * @covers \ParserCache::get
338 public function testDoesNotGetNotSafeToCache() {
339 $cache = $this->createParserCache();
340 $parserOutput = new ParserOutput( 'TEST_TEXT' );
341 $parserOutput->recordOption( 'wrapclass' );
343 $cache->save( $parserOutput, $this->page, ParserOptions::newFromAnon(), $this->cacheTime );
345 $otherOptions = ParserOptions::newFromAnon();
346 $otherOptions->setOption( 'wrapclass', 'wrapwrap' );
348 $this->assertFalse( $cache->get( $this->page, $otherOptions ) );
349 $this->assertFalse( $cache->get( $this->page, $otherOptions, true ) );
350 $this->assertFalse( $cache->getDirty( $this->page, $otherOptions ) );
354 * Test that ParserOptions::isSafeToCache is respected on save
355 * @covers \ParserCache::save
356 * @covers \ParserCache::get
358 public function testStoresNotSafeToCacheAndUnused() {
359 $cache = $this->createParserCache();
360 $parserOutput = new ParserOutput( 'TEST_TEXT' );
362 $options = ParserOptions::newFromAnon();
363 $options->setOption( 'wrapclass', 'wrapwrap' );
365 $cache->save( $parserOutput, $this->page, $options, $this->cacheTime );
366 $this->assertStringContainsString( 'TEST_TEXT', $cache->get( $this->page, $options )->getText() );
370 * Test that fetching with different used option don't return a value.
371 * @covers \ParserCache::get
372 * @covers \ParserCache::save
374 public function testSaveGetDifferentUsedOption() {
375 $cache = $this->createParserCache();
376 $parserOutput = new ParserOutput( 'TEST_TEXT' );
377 $optionName = $this->getDummyUsedOptions()[0];
378 $parserOutput->recordOption( $optionName );
380 $options1 = ParserOptions::newFromAnon();
381 $options1->setOption( $optionName, 'value1' );
382 $cache->save( $parserOutput, $this->page, $options1, $this->cacheTime );
384 $options2 = ParserOptions::newFromAnon();
385 $options2->setOption( $optionName, 'value2' );
386 $this->assertFalse( $cache->get( $this->page, $options2 ) );
390 * Test that output with expired metadata can be retrieved with getDirty
391 * @covers \ParserCache::getDirty
392 * @covers \ParserCache::get
394 public function testGetExpiredMetadata() {
395 $cache = $this->createParserCache();
396 $parserOutput = new ParserOutput( 'TEST_TEXT' );
397 $parserOutput->updateCacheExpiry( 10 );
399 $options1 = ParserOptions::newFromAnon();
400 $cache->save( $parserOutput, $this->page, $options1, $this->cacheTime );
402 MWTimestamp::setFakeTime( $this->time + 15 * 1000 );
403 $this->assertFalse( $cache->get( $this->page, $options1 ) );
404 $this->assertInstanceOf( ParserOutput::class,
405 $cache->get( $this->page, $options1, true ) );
406 $this->assertInstanceOf( ParserOutput::class,
407 $cache->getDirty( $this->page, $options1 ) );
411 * Test that expired output with not expired metadata can be retrieved with getDirty
412 * @covers \ParserCache::getDirty
413 * @covers \ParserCache::get
415 public function testGetExpiredContent() {
416 $cache = $this->createParserCache();
417 $optionName = $this->getDummyUsedOptions()[0];
419 $parserOutput1 = new ParserOutput( 'TEST_TEXT1' );
420 $parserOutput1->recordOption( $optionName );
421 $parserOutput1->updateCacheExpiry( 10 );
422 $options1 = ParserOptions::newFromAnon();
423 $options1->setOption( $optionName, 'value1' );
424 $cache->save( $parserOutput1, $this->page, $options1, $this->cacheTime );
426 $parserOutput2 = new ParserOutput( 'TEST_TEXT2' );
427 $parserOutput2->recordOption( $optionName );
428 $parserOutput2->updateCacheExpiry( 100500600 );
429 $options2 = ParserOptions::newFromAnon();
430 $options2->setOption( $optionName, 'value2' );
431 $cache->save( $parserOutput2, $this->page, $options2, $this->cacheTime );
433 MWTimestamp::setFakeTime( $this->time + 15 * 1000 );
434 $this->assertFalse( $cache->get( $this->page, $options1 ) );
435 $this->assertInstanceOf( ParserOutput::class,
436 $cache->get( $this->page, $options1, true ) );
437 $this->assertInstanceOf( ParserOutput::class,
438 $cache->getDirty( $this->page, $options1 ) );
442 * Test that output with outdated metadata can be retrieved with getDirty
443 * @covers \ParserCache::getDirty
444 * @covers \ParserCache::get
446 public function testGetOutdatedMetadata() {
447 $cache = $this->createParserCache();
448 $parserOutput = new ParserOutput( 'TEST_TEXT' );
450 $options1 = ParserOptions::newFromAnon();
451 $cache->save( $parserOutput, $this->page, $options1, $this->cacheTime );
452 $this->assertInstanceOf( ParserOutput::class,
453 $cache->get( $this->page, $options1 ) );
455 $this->page = $this->createPageRecord( [ 'page_latest' => $this->page->getLatest() + 1 ] );
456 $this->assertFalse( $cache->get( $this->page, $options1 ) );
457 $this->assertInstanceOf( ParserOutput::class,
458 $cache->get( $this->page, $options1, true ) );
459 $this->assertInstanceOf( ParserOutput::class,
460 $cache->getDirty( $this->page, $options1 ) );
464 * Test that outdated output with good metadata can be retrieved with getDirty
465 * @covers \ParserCache::getDirty
466 * @covers \ParserCache::get
468 public function testGetOutdatedContent() {
469 $cache = $this->createParserCache();
470 $optionName = $this->getDummyUsedOptions()[0];
472 $parserOutput1 = new ParserOutput( 'TEST_TEXT' );
473 $parserOutput1->recordOption( $optionName );
474 $options1 = ParserOptions::newFromAnon();
475 $options1->setOption( $optionName, 'value1' );
476 $cache->save( $parserOutput1, $this->page, $options1, $this->cacheTime );
478 $this->page = $this->createPageRecord( [ 'page_latest' => $this->page->getLatest() + 1 ] );
479 $parserOutput2 = new ParserOutput( 'TEST_TEXT' );
480 $parserOutput2->recordOption( $optionName );
481 $options2 = ParserOptions::newFromAnon();
482 $options2->setOption( $optionName, 'value2' );
483 $cache->save( $parserOutput2, $this->page, $options2, $this->cacheTime );
485 $this->assertFalse( $cache->get( $this->page, $options1 ) );
486 $this->assertInstanceOf( ParserOutput::class,
487 $cache->get( $this->page, $options1, true ) );
488 $this->assertInstanceOf( ParserOutput::class,
489 $cache->getDirty( $this->page, $options1 ) );
493 * Test that fetching after deleting a key returns false.
494 * @covers \ParserCache::deleteOptionsKey
496 public function testDeleteOptionsKey() {
497 $cache = $this->createParserCache();
498 $parserOutput = new ParserOutput( 'TEST_TEXT' );
500 $options1 = ParserOptions::newFromAnon();
501 $cache->save( $parserOutput, $this->page, $options1, $this->cacheTime );
502 $this->assertInstanceOf( ParserOutput::class,
503 $cache->get( $this->page, $options1 ) );
504 $cache->deleteOptionsKey( $this->page );
506 $this->assertFalse( $cache->get( $this->page, $options1 ) );
510 * Test that RejectParserCacheValue hook can reject ParserOutput
511 * @covers \ParserCache::get
513 public function testRejectedByHook() {
514 $parserOutput = new ParserOutput( 'TEST_TEXT' );
515 $options = ParserOptions::newFromAnon();
516 $options->setOption( $this->getDummyUsedOptions()[0], 'value1' );
518 $wikiPageMock = $this->createMock( WikiPage::class );
519 $wikiPageMock->method( 'getContentModel' )->willReturn( CONTENT_MODEL_WIKITEXT );
520 $wikiPageFactory = $this->createMock( WikiPageFactory::class );
521 $wikiPageFactory->method( 'newFromTitle' )
522 ->with( $this->page )
523 ->willReturn( $wikiPageMock );
524 $hookContainer = $this->createHookContainer( [
525 'RejectParserCacheValue' =>
526 function ( ParserOutput $value, WikiPage $hookPage, ParserOptions $popts )
527 use ( $wikiPageMock, $parserOutput, $options ) {
528 $this->assertEquals( $parserOutput, $value );
529 $this->assertSame( $wikiPageMock, $hookPage );
530 $this->assertSame( $options, $popts );
531 return false;
533 ] );
534 $cache = $this->createParserCache( $hookContainer, null, null, $wikiPageFactory );
535 $cache->save( $parserOutput, $this->page, $options, $this->cacheTime );
536 $this->assertFalse( $cache->get( $this->page, $options ) );
540 * Test that ParserCacheSaveComplete hook is run
541 * @covers \ParserCache::save
543 public function testParserCacheSaveCompleteHook() {
544 $parserOutput = new ParserOutput( 'TEST_TEXT' );
545 $options = ParserOptions::newFromAnon();
546 $options->setOption( $this->getDummyUsedOptions()[0], 'value1' );
548 $hookContainer = $this->createHookContainer( [
549 'ParserCacheSaveComplete' =>
550 function (
551 ParserCache $hookCache, ParserOutput $value, Title $hookTitle, ParserOptions $popts, int $revId
552 ) use ( $parserOutput, $options ) {
553 $this->assertSame( $parserOutput, $value );
554 $this->assertSame( $options, $popts );
555 $this->assertSame( 42, $revId );
557 ] );
558 $cache = $this->createParserCache( $hookContainer );
559 $cache->save( $parserOutput, $this->page, $options, $this->cacheTime, 42 );
563 * Tests that parser cache respects skipped if page does not exist
564 * @covers \ParserCache::get
566 public function testSkipIfNotExist() {
567 $mockPage = $this->createNoOpMock( PageRecord::class, [ 'exists', 'assertWiki' ] );
568 $mockPage->method( 'exists' )->willReturn( false );
569 $wikiPageMock = $this->createMock( WikiPage::class );
570 $wikiPageMock->method( 'getContentModel' )->willReturn( 'wikitext' );
571 $wikiPageFactoryMock = $this->createMock( WikiPageFactory::class );
572 $wikiPageFactoryMock->method( 'newFromTitle' )
573 ->with( $mockPage )
574 ->willReturn( $wikiPageMock );
575 $cache = $this->createParserCache( null, null, null, $wikiPageFactoryMock );
576 $this->assertFalse( $cache->get( $mockPage, ParserOptions::newFromAnon() ) );
580 * Tests that parser cache respects skipped if page is redirect
581 * @covers \ParserCache::get
583 public function testSkipIfRedirect() {
584 $cache = $this->createParserCache();
585 $page = $this->createPageRecord( [
586 'page_is_redirect' => true
587 ] );
588 $this->assertFalse( $cache->get( $page, ParserOptions::newFromAnon() ) );
592 * Tests that getCacheStorage returns underlying BagOStuff
593 * @covers \ParserCache::getCacheStorage
595 public function testGetCacheStorage() {
596 $storage = new EmptyBagOStuff();
597 $cache = $this->createParserCache( null, $storage );
598 $this->assertSame( $storage, $cache->getCacheStorage() );
602 * @covers \ParserCache::save
604 public function testSaveNoText() {
605 $this->expectException( InvalidArgumentException::class );
606 $this->createParserCache()->save(
607 new ParserOutput( null ),
608 $this->page,
609 ParserOptions::newFromAnon()
613 public static function provideCorruptData() {
614 yield 'JSON serialization, bad data' => [ 'bla bla' ];
615 yield 'JSON serialization, no _class_' => [ '{"test":"test"}' ];
616 yield 'JSON serialization, non-existing _class_' => [ '{"_class_":"NonExistentBogusClass"}' ];
617 $wrongInstance = new JsonDeserializableSuperClass( 'test' );
618 yield 'JSON serialization, wrong class' => [ json_encode( $wrongInstance->jsonSerialize() ) ];
622 * Test that we handle corrupt data gracefully.
623 * This is important for forward-compatibility with JSON serialization.
624 * We want to be sure that we don't crash horribly if we have to roll
625 * back to a version of the code that doesn't know about JSON.
627 * @dataProvider provideCorruptData
628 * @covers \ParserCache::get
629 * @covers \ParserCache::restoreFromJson
630 * @param string $data
632 public function testCorruptData( string $data ) {
633 $cache = $this->createParserCache( null, new HashBagOStuff() );
634 $parserOutput = new ParserOutput( 'TEST_TEXT' );
636 $options1 = ParserOptions::newFromAnon();
637 $cache->save( $parserOutput, $this->page, $options1, $this->cacheTime );
639 $outputKey = $cache->makeParserOutputKey(
640 $this->page,
641 $options1,
642 $parserOutput->getUsedOptions()
645 $cache->getCacheStorage()->set( $outputKey, $data );
647 // just make sure we don't crash and burn
648 $this->assertFalse( $cache->get( $this->page, $options1 ) );
652 * Test that we handle corrupt data gracefully.
653 * This is important for forward-compatibility with JSON serialization.
654 * We want to be sure that we don't crash horribly if we have to roll
655 * back to a version of the code that doesn't know about JSON.
657 * @covers \ParserCache::getMetadata
659 public function testCorruptMetadata() {
660 $cacheStorage = new HashBagOStuff();
661 $cache = $this->createParserCache( null, $cacheStorage );
662 $parserOutput = new ParserOutput( 'TEST_TEXT' );
664 $options1 = ParserOptions::newFromAnon();
665 $cache->save( $parserOutput, $this->page, $options1, $this->cacheTime );
667 // Mess up the metadata
668 $optionsKey = TestingAccessWrapper::newFromObject( $cache )->makeMetadataKey(
669 $this->page
671 $cacheStorage->set( $optionsKey, 'bad data' );
673 // Recreate the cache to drop in-memory cached metadata.
674 $cache = $this->createParserCache( null, $cacheStorage );
676 // just make sure we don't crash and burn
677 $this->assertNull( $cache->getMetadata( $this->page ) );
681 * Test what happens when upgrading from 1.35 or earlier,
682 * when old cache entries do not yet use JSON.
684 * @covers \ParserCache::get
686 public function testMigrationToJson() {
687 $bagOStuff = new HashBagOStuff();
689 $wikiPageMock = $this->createMock( WikiPage::class );
690 $wikiPageMock->method( 'getContentModel' )->willReturn( CONTENT_MODEL_WIKITEXT );
691 $wikiPageFactory = $this->createMock( WikiPageFactory::class );
692 $wikiPageFactory->method( 'newFromTitle' )->willReturn( $wikiPageMock );
693 $globalIdGenerator = $this->createMock( GlobalIdGenerator::class );
694 $globalIdGenerator->method( 'newUUIDv1' )->willReturn( 'uuid-uuid' );
695 $cache = $this->getMockBuilder( ParserCache::class )
696 ->setConstructorArgs( [
697 'test',
698 $bagOStuff,
699 '19900220000000',
700 $this->createHookContainer( [] ),
701 new JsonCodec(),
702 StatsFactory::newNull(),
703 new NullLogger(),
704 $this->createMock( TitleFactory::class ),
705 $wikiPageFactory,
706 $globalIdGenerator
708 ->onlyMethods( [ 'convertForCache' ] )
709 ->getMock();
711 // Emulate pre-1.36 behavior: rely on native PHP serialization.
712 // Note that backwards compatibility of the actual serialization is covered
713 // by ParserOutputTest which uses various versions of serialized data
714 // under tests/phpunit/data/ParserCache.
715 $cache->method( 'convertForCache' )->willReturnCallback(
716 static function ( CacheTime $obj, string $key ) {
717 return $obj;
721 $parserOutput1 = new ParserOutput( 'Lorem Ipsum' );
723 $options = ParserOptions::newFromAnon();
724 $cache->save( $parserOutput1, $this->page, $options, $this->cacheTime );
726 // emulate migration to JSON
727 $cache = $this->createParserCache( null, $bagOStuff );
729 // make sure we can load non-json cache data
730 $cachedOutput = $cache->get( $this->page, $options );
731 $this->assertEquals( $parserOutput1, $cachedOutput );
733 // now test that the cache works when using JSON
734 $parserOutput2 = new ParserOutput( 'dolor sit amet' );
735 $cache->save( $parserOutput2, $this->page, $options, $this->cacheTime );
737 // make sure we can load json cache data
738 $cachedOutput = $cache->get( $this->page, $options );
739 $this->assertEquals( $parserOutput2, $cachedOutput );
743 * @covers \ParserCache::convertForCache
745 public function testNonSerializableJsonIsReported() {
746 $testLogger = new TestLogger( true );
747 $cache = $this->createParserCache( null, null, $testLogger );
749 $parserOutput = $this->createDummyParserOutput();
750 $parserOutput->setExtensionData( 'test', new User() );
751 $cache->save( $parserOutput, $this->page, ParserOptions::newFromAnon() );
752 $this->assertArraySubmapSame(
753 [ [ LogLevel::ERROR, 'Unable to serialize JSON' ] ],
754 $testLogger->getBuffer()
759 * @covers \ParserCache::convertForCache
761 public function testCyclicStructuresDoNotBlowUpInJson() {
762 $this->markTestSkipped( 'Temporarily disabled: T314338' );
763 $testLogger = new TestLogger( true );
764 $cache = $this->createParserCache( null, null, $testLogger );
766 $parserOutput = $this->createDummyParserOutput();
767 $cyclicArray = [ 'a' => 'b' ];
768 $cyclicArray['c'] = &$cyclicArray;
769 $parserOutput->setExtensionData( 'test', $cyclicArray );
770 $cache->save( $parserOutput, $this->page, ParserOptions::newFromAnon() );
771 $this->assertArraySubmapSame(
772 [ [ LogLevel::ERROR, 'Unable to serialize JSON' ] ],
773 $testLogger->getBuffer()
778 * Tests that unicode characters are not \u escaped
780 * @covers \ParserCache::convertForCache
782 public function testJsonEncodeUnicode() {
783 $unicodeCharacter = "Э";
784 $cache = $this->createParserCache( null, new HashBagOStuff() );
786 $parserOutput = $this->createDummyParserOutput();
787 $parserOutput->setRawText( $unicodeCharacter );
788 $cache->save( $parserOutput, $this->page, ParserOptions::newFromAnon() );
789 $json = $cache->getCacheStorage()->get(
790 $cache->makeParserOutputKey( $this->page, ParserOptions::newFromAnon() )
792 $this->assertStringNotContainsString( "\u003E", $json );
793 $this->assertStringContainsString( $unicodeCharacter, $json );