Merge "Exclude redirects from Special:Fewestrevisions"
[lhc/web/wiklou.git] / tests / phpunit / includes / cache / MessageCacheTest.php
1 <?php
2
3 use MediaWiki\MediaWikiServices;
4 use Wikimedia\TestingAccessWrapper;
5
6 /**
7 * @group Database
8 * @group Cache
9 * @covers MessageCache
10 */
11 class MessageCacheTest extends MediaWikiLangTestCase {
12
13 protected function setUp() {
14 parent::setUp();
15 $this->configureLanguages();
16 MessageCache::destroyInstance();
17 MessageCache::singleton()->enable();
18 }
19
20 /**
21 * Helper function -- setup site language for testing
22 */
23 protected function configureLanguages() {
24 // for the test, we need the content language to be anything but English,
25 // let's choose e.g. German (de)
26 $this->setUserLang( 'de' );
27 $this->setContentLang( 'de' );
28 }
29
30 function addDBDataOnce() {
31 $this->configureLanguages();
32
33 // Set up messages and fallbacks ab -> ru -> de
34 $this->makePage( 'FallbackLanguageTest-Full', 'ab' );
35 $this->makePage( 'FallbackLanguageTest-Full', 'ru' );
36 $this->makePage( 'FallbackLanguageTest-Full', 'de' );
37
38 // Fallbacks where ab does not exist
39 $this->makePage( 'FallbackLanguageTest-Partial', 'ru' );
40 $this->makePage( 'FallbackLanguageTest-Partial', 'de' );
41
42 // Fallback to the content language
43 $this->makePage( 'FallbackLanguageTest-ContLang', 'de' );
44
45 // Add customizations for an existing message.
46 $this->makePage( 'sunday', 'ru' );
47
48 // Full key tests -- always want russian
49 $this->makePage( 'MessageCacheTest-FullKeyTest', 'ab' );
50 $this->makePage( 'MessageCacheTest-FullKeyTest', 'ru' );
51
52 // In content language -- get base if no derivative
53 $this->makePage( 'FallbackLanguageTest-NoDervContLang', 'de', 'de/none' );
54 }
55
56 /**
57 * Helper function for addDBData -- adds a simple page to the database
58 *
59 * @param string $title Title of page to be created
60 * @param string $lang Language and content of the created page
61 * @param string|null $content Content of the created page, or null for a generic string
62 *
63 * @return Revision
64 */
65 protected function makePage( $title, $lang, $content = null ) {
66 if ( $content === null ) {
67 $content = $lang;
68 }
69 if ( $lang !== MediaWikiServices::getInstance()->getContentLanguage()->getCode() ) {
70 $title = "$title/$lang";
71 }
72
73 $title = Title::newFromText( $title, NS_MEDIAWIKI );
74 $wikiPage = new WikiPage( $title );
75 $contentHandler = ContentHandler::makeContent( $content, $title );
76 $status = $wikiPage->doEditContent( $contentHandler, "$lang translation test case" );
77
78 // sanity
79 $this->assertTrue( $status->isOK(), 'Create page ' . $title->getPrefixedDBkey() );
80 return $status->value['revision'];
81 }
82
83 /**
84 * Test message fallbacks, T3495
85 *
86 * @dataProvider provideMessagesForFallback
87 */
88 public function testMessageFallbacks( $message, $lang, $expectedContent ) {
89 $result = MessageCache::singleton()->get( $message, true, $lang );
90 $this->assertEquals( $expectedContent, $result, "Message fallback failed." );
91 }
92
93 function provideMessagesForFallback() {
94 return [
95 [ 'FallbackLanguageTest-Full', 'ab', 'ab' ],
96 [ 'FallbackLanguageTest-Partial', 'ab', 'ru' ],
97 [ 'FallbackLanguageTest-ContLang', 'ab', 'de' ],
98 [ 'FallbackLanguageTest-None', 'ab', false ],
99
100 // Existing message with customizations on the fallbacks
101 [ 'sunday', 'ab', 'амҽыш' ],
102
103 // T48579
104 [ 'FallbackLanguageTest-NoDervContLang', 'de', 'de/none' ],
105 // UI language different from content language should only use de/none as last option
106 [ 'FallbackLanguageTest-NoDervContLang', 'fit', 'de/none' ],
107 ];
108 }
109
110 public function testReplaceMsg() {
111 $messageCache = MessageCache::singleton();
112 $message = 'go';
113 $uckey = MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $message );
114 $oldText = $messageCache->get( $message ); // "Ausführen"
115
116 $dbw = wfGetDB( DB_MASTER );
117 $dbw->startAtomic( __METHOD__ ); // simulate request and block deferred updates
118 $messageCache->replace( $uckey, 'Allez!' );
119 $this->assertEquals( 'Allez!',
120 $messageCache->getMsgFromNamespace( $uckey, 'de' ),
121 'Updates are reflected in-process immediately' );
122 $this->assertEquals( 'Allez!',
123 $messageCache->get( $message ),
124 'Updates are reflected in-process immediately' );
125 $this->makePage( 'Go', 'de', 'Race!' );
126 $dbw->endAtomic( __METHOD__ );
127
128 $this->assertEquals( 0,
129 DeferredUpdates::pendingUpdatesCount(),
130 'Post-commit deferred update triggers a run of all updates' );
131
132 $this->assertEquals( 'Race!', $messageCache->get( $message ), 'Correct final contents' );
133
134 $this->makePage( 'Go', 'de', $oldText );
135 $messageCache->replace( $uckey, $oldText ); // deferred update runs immediately
136 $this->assertEquals( $oldText, $messageCache->get( $message ), 'Content restored' );
137 }
138
139 public function testReplaceCache() {
140 global $wgWANObjectCaches;
141
142 // We need a WAN cache for this.
143 $this->setMwGlobals( [
144 'wgMainWANCache' => 'hash',
145 'wgWANObjectCaches' => $wgWANObjectCaches + [
146 'hash' => [
147 'class' => WANObjectCache::class,
148 'cacheId' => 'hash',
149 'channels' => []
150 ]
151 ]
152 ] );
153 $this->overrideMwServices();
154
155 MessageCache::destroyInstance();
156 $messageCache = MessageCache::singleton();
157 $messageCache->enable();
158
159 // Populate one key
160 $this->makePage( 'Key1', 'de', 'Value1' );
161 $this->assertEquals( 0,
162 DeferredUpdates::pendingUpdatesCount(),
163 'Post-commit deferred update triggers a run of all updates' );
164 $this->assertEquals( 'Value1', $messageCache->get( 'Key1' ), 'Key1 was successfully edited' );
165
166 // Screw up the database so MessageCache::loadFromDB() will
167 // produce the wrong result for reloading Key1
168 $this->db->delete(
169 'page', [ 'page_namespace' => NS_MEDIAWIKI, 'page_title' => 'Key1' ], __METHOD__
170 );
171
172 // Populate the second key
173 $this->makePage( 'Key2', 'de', 'Value2' );
174 $this->assertEquals( 0,
175 DeferredUpdates::pendingUpdatesCount(),
176 'Post-commit deferred update triggers a run of all updates' );
177 $this->assertEquals( 'Value2', $messageCache->get( 'Key2' ), 'Key2 was successfully edited' );
178
179 // Now test that the second edit didn't reload Key1
180 $this->assertEquals( 'Value1', $messageCache->get( 'Key1' ),
181 'Key1 wasn\'t reloaded by edit of Key2' );
182 }
183
184 /**
185 * @dataProvider provideNormalizeKey
186 */
187 public function testNormalizeKey( $key, $expected ) {
188 $actual = MessageCache::normalizeKey( $key );
189 $this->assertEquals( $expected, $actual );
190 }
191
192 public function provideNormalizeKey() {
193 return [
194 [ 'Foo', 'foo' ],
195 [ 'foo', 'foo' ],
196 [ 'fOo', 'fOo' ],
197 [ 'FOO', 'fOO' ],
198 [ 'Foo bar', 'foo_bar' ],
199 [ 'Ćab', 'ćab' ],
200 [ 'Ćab_e 3', 'ćab_e_3' ],
201 [ 'ĆAB', 'ćAB' ],
202 [ 'ćab', 'ćab' ],
203 [ 'ćaB', 'ćaB' ],
204 ];
205 }
206
207 public function testNoDBAccessContentLanguage() {
208 global $wgContLanguageCode;
209
210 $dbr = wfGetDB( DB_REPLICA );
211
212 MessageCache::singleton()->getMsgFromNamespace( 'allpages', $wgContLanguageCode );
213
214 $this->assertEquals( 0, $dbr->trxLevel() );
215 $dbr->setFlag( DBO_TRX, $dbr::REMEMBER_PRIOR ); // make queries trigger TRX
216
217 MessageCache::singleton()->getMsgFromNamespace( 'go', $wgContLanguageCode );
218
219 $dbr->restoreFlags();
220
221 $this->assertEquals( 0, $dbr->trxLevel(), "No DB read queries (content language)" );
222 }
223
224 public function testNoDBAccessNonContentLanguage() {
225 $dbr = wfGetDB( DB_REPLICA );
226
227 MessageCache::singleton()->getMsgFromNamespace( 'allpages/nl', 'nl' );
228
229 $this->assertEquals( 0, $dbr->trxLevel() );
230 $dbr->setFlag( DBO_TRX, $dbr::REMEMBER_PRIOR ); // make queries trigger TRX
231
232 MessageCache::singleton()->getMsgFromNamespace( 'go/nl', 'nl' );
233
234 $dbr->restoreFlags();
235
236 $this->assertEquals( 0, $dbr->trxLevel(), "No DB read queries (non-content language)" );
237 }
238
239 /**
240 * Regression test for T218918
241 */
242 public function testLoadFromDB_fetchLatestRevision() {
243 // Create three revisions of the same message page.
244 // Must be an existing message key.
245 $key = 'Log';
246 $this->makePage( $key, 'de', 'Test eins' );
247 $this->makePage( $key, 'de', 'Test zwei' );
248 $r3 = $this->makePage( $key, 'de', 'Test drei' );
249
250 // Create an out-of-sequence revision by importing a
251 // revision with an old timestamp. Hacky.
252 $importRevision = new WikiRevision( new HashConfig() );
253 $importRevision->setTitle( $r3->getTitle() );
254 $importRevision->setComment( 'Imported edit' );
255 $importRevision->setTimestamp( '19991122001122' );
256 $importRevision->setText( 'IMPORTED OLD TEST' );
257 $importRevision->setUsername( 'ext>Alan Smithee' );
258
259 $importer = MediaWikiServices::getInstance()->getWikiRevisionOldRevisionImporterNoUpdates();
260 $importer->import( $importRevision );
261
262 // Now, load the message from the wiki page
263 MessageCache::destroyInstance();
264 $messageCache = MessageCache::singleton();
265 $messageCache->enable();
266 $messageCache = TestingAccessWrapper::newFromObject( $messageCache );
267
268 $cache = $messageCache->loadFromDB( 'de' );
269
270 $this->assertArrayHasKey( $key, $cache );
271
272 // Text in the cache has an extra space in front!
273 $this->assertSame( ' ' . 'Test drei', $cache[$key] );
274 }
275
276 }