Do not insert page titles into querycache.qc_value
[lhc/web/wiklou.git] / tests / phpunit / includes / PrefixSearchTest.php
1 <?php
2
3 use Wikimedia\TestingAccessWrapper;
4
5 /**
6 * @group Search
7 * @group Database
8 * @covers PrefixSearch
9 */
10 class PrefixSearchTest extends MediaWikiLangTestCase {
11 const NS_NONCAP = 12346;
12
13 private $originalHandlers;
14
15 public function addDBDataOnce() {
16 if ( !$this->isWikitextNS( NS_MAIN ) ) {
17 // tests are skipped if NS_MAIN is not wikitext
18 return;
19 }
20
21 $this->insertPage( 'Sandbox' );
22 $this->insertPage( 'Bar' );
23 $this->insertPage( 'Example' );
24 $this->insertPage( 'Example Bar' );
25 $this->insertPage( 'Example Foo' );
26 $this->insertPage( 'Example Foo/Bar' );
27 $this->insertPage( 'Example/Baz' );
28 $this->insertPage( 'Redirect test', '#REDIRECT [[Redirect Test]]' );
29 $this->insertPage( 'Redirect Test' );
30 $this->insertPage( 'Redirect Test Worse Result' );
31 $this->insertPage( 'Redirect test2', '#REDIRECT [[Redirect Test2]]' );
32 $this->insertPage( 'Redirect TEST2', '#REDIRECT [[Redirect Test2]]' );
33 $this->insertPage( 'Redirect Test2' );
34 $this->insertPage( 'Redirect Test2 Worse Result' );
35
36 $this->insertPage( 'Talk:Sandbox' );
37 $this->insertPage( 'Talk:Example' );
38
39 $this->insertPage( 'User:Example' );
40
41 $this->insertPage( Title::makeTitle( self::NS_NONCAP, 'Bar' ) );
42 $this->insertPage( Title::makeTitle( self::NS_NONCAP, 'Upper' ) );
43 $this->insertPage( Title::makeTitle( self::NS_NONCAP, 'sandbox' ) );
44 }
45
46 protected function setUp() {
47 parent::setUp();
48
49 if ( !$this->isWikitextNS( NS_MAIN ) ) {
50 $this->markTestSkipped( 'Main namespace does not support wikitext.' );
51 }
52
53 // Avoid special pages from extensions interfering with the tests
54 $this->setMwGlobals( [
55 'wgSpecialPages' => [],
56 'wgHooks' => [],
57 'wgExtraNamespaces' => [ self::NS_NONCAP => 'NonCap' ],
58 'wgCapitalLinkOverrides' => [ self::NS_NONCAP => false ],
59 ] );
60
61 $this->originalHandlers = TestingAccessWrapper::newFromClass( Hooks::class )->handlers;
62 TestingAccessWrapper::newFromClass( Hooks::class )->handlers = [];
63 }
64
65 public function tearDown() {
66 parent::tearDown();
67
68 TestingAccessWrapper::newFromClass( Hooks::class )->handlers = $this->originalHandlers;
69 }
70
71 protected function searchProvision( array $results = null ) {
72 if ( $results === null ) {
73 $this->setMwGlobals( 'wgHooks', [] );
74 } else {
75 $this->setMwGlobals( 'wgHooks', [
76 'PrefixSearchBackend' => [
77 function ( $namespaces, $search, $limit, &$srchres ) use ( $results ) {
78 $srchres = $results;
79 return false;
80 }
81 ],
82 ] );
83 }
84 }
85
86 public static function provideSearch() {
87 return [
88 [ [
89 'Empty string',
90 'query' => '',
91 'results' => [],
92 ] ],
93 [ [
94 'Main namespace with title prefix',
95 'query' => 'Ex',
96 'results' => [
97 'Example',
98 'Example/Baz',
99 'Example Bar',
100 ],
101 // Third result when testing offset
102 'offsetresult' => [
103 'Example Foo',
104 ],
105 ] ],
106 [ [
107 'Talk namespace prefix',
108 'query' => 'Talk:',
109 'results' => [
110 'Talk:Example',
111 'Talk:Sandbox',
112 ],
113 ] ],
114 [ [
115 'User namespace prefix',
116 'query' => 'User:',
117 'results' => [
118 'User:Example',
119 ],
120 ] ],
121 [ [
122 'Special namespace prefix',
123 'query' => 'Special:',
124 'results' => [
125 'Special:ActiveUsers',
126 'Special:AllMessages',
127 'Special:AllMyUploads',
128 ],
129 // Third result when testing offset
130 'offsetresult' => [
131 'Special:AllPages',
132 ],
133 ] ],
134 [ [
135 'Special namespace with prefix',
136 'query' => 'Special:Un',
137 'results' => [
138 'Special:Unblock',
139 'Special:UncategorizedCategories',
140 'Special:UncategorizedFiles',
141 ],
142 // Third result when testing offset
143 'offsetresult' => [
144 'Special:UncategorizedPages',
145 ],
146 ] ],
147 [ [
148 'Special page name',
149 'query' => 'Special:EditWatchlist',
150 'results' => [
151 'Special:EditWatchlist',
152 ],
153 ] ],
154 [ [
155 'Special page subpages',
156 'query' => 'Special:EditWatchlist/',
157 'results' => [
158 'Special:EditWatchlist/clear',
159 'Special:EditWatchlist/raw',
160 ],
161 ] ],
162 [ [
163 'Special page subpages with prefix',
164 'query' => 'Special:EditWatchlist/cl',
165 'results' => [
166 'Special:EditWatchlist/clear',
167 ],
168 ] ],
169 [ [
170 'Namespace with case sensitive first letter',
171 'query' => 'NonCap:upper',
172 'results' => []
173 ] ],
174 [ [
175 'Multinamespace search',
176 'query' => 'B',
177 'results' => [
178 'Bar',
179 'NonCap:Bar',
180 ],
181 'namespaces' => [ NS_MAIN, self::NS_NONCAP ],
182 ] ],
183 [ [
184 'Multinamespace search with lowercase first letter',
185 'query' => 'sand',
186 'results' => [
187 'Sandbox',
188 'NonCap:sandbox',
189 ],
190 'namespaces' => [ NS_MAIN, self::NS_NONCAP ],
191 ] ],
192 ];
193 }
194
195 /**
196 * @dataProvider provideSearch
197 * @covers PrefixSearch::search
198 * @covers PrefixSearch::searchBackend
199 */
200 public function testSearch( array $case ) {
201 // FIXME: fails under postgres
202 $this->markTestSkippedIfDbType( 'postgres' );
203 $this->searchProvision( null );
204
205 $namespaces = $case['namespaces'] ?? [];
206
207 if ( wfGetDB( DB_REPLICA )->getType() === 'postgres' ) {
208 // Postgres will sort lexicographically on utf8 code units (" " before "/")
209 sort( $case['results'], SORT_STRING );
210 }
211
212 $searcher = new StringPrefixSearch;
213 $results = $searcher->search( $case['query'], 3, $namespaces );
214 $this->assertEquals(
215 $case['results'],
216 $results,
217 $case[0]
218 );
219 }
220
221 /**
222 * @dataProvider provideSearch
223 * @covers PrefixSearch::search
224 * @covers PrefixSearch::searchBackend
225 */
226 public function testSearchWithOffset( array $case ) {
227 // FIXME: fails under postgres
228 $this->markTestSkippedIfDbType( 'postgres' );
229 $this->searchProvision( null );
230
231 $namespaces = $case['namespaces'] ?? [];
232
233 $searcher = new StringPrefixSearch;
234 $results = $searcher->search( $case['query'], 3, $namespaces, 1 );
235
236 if ( wfGetDB( DB_REPLICA )->getType() === 'postgres' ) {
237 // Postgres will sort lexicographically on utf8 code units (" " before "/")
238 sort( $case['results'], SORT_STRING );
239 }
240
241 // We don't expect the first result when offsetting
242 array_shift( $case['results'] );
243 // And sometimes we expect a different last result
244 $expected = isset( $case['offsetresult'] ) ?
245 array_merge( $case['results'], $case['offsetresult'] ) :
246 $case['results'];
247
248 $this->assertEquals(
249 $expected,
250 $results,
251 $case[0]
252 );
253 }
254
255 public static function provideSearchBackend() {
256 return [
257 [ [
258 'Simple case',
259 'provision' => [
260 'Bar',
261 'Barcelona',
262 'Barbara',
263 ],
264 'query' => 'Bar',
265 'results' => [
266 'Bar',
267 'Barcelona',
268 'Barbara',
269 ],
270 ] ],
271 [ [
272 'Exact match not on top (T72958)',
273 'provision' => [
274 'Barcelona',
275 'Bar',
276 'Barbara',
277 ],
278 'query' => 'Bar',
279 'results' => [
280 'Bar',
281 'Barcelona',
282 'Barbara',
283 ],
284 ] ],
285 [ [
286 'Exact match missing (T72958)',
287 'provision' => [
288 'Barcelona',
289 'Barbara',
290 'Bart',
291 ],
292 'query' => 'Bar',
293 'results' => [
294 'Bar',
295 'Barcelona',
296 'Barbara',
297 ],
298 ] ],
299 [ [
300 'Exact match missing and not existing',
301 'provision' => [
302 'Exile',
303 'Exist',
304 'External',
305 ],
306 'query' => 'Ex',
307 'results' => [
308 'Exile',
309 'Exist',
310 'External',
311 ],
312 ] ],
313 [ [
314 "Exact match shouldn't override already found match if " .
315 "exact is redirect and found isn't",
316 'provision' => [
317 // Target of the exact match is low in the list
318 'Redirect Test Worse Result',
319 'Redirect Test',
320 ],
321 'query' => 'redirect test',
322 'results' => [
323 // Redirect target is pulled up and exact match isn't added
324 'Redirect Test',
325 'Redirect Test Worse Result',
326 ],
327 ] ],
328 [ [
329 "Exact match shouldn't override already found match if " .
330 "both exact match and found match are redirect",
331 'provision' => [
332 // Another redirect to the same target as the exact match
333 // is low in the list
334 'Redirect Test2 Worse Result',
335 'Redirect test2',
336 ],
337 'query' => 'redirect TEST2',
338 'results' => [
339 // Found redirect is pulled to the top and exact match isn't
340 // added
341 'Redirect test2',
342 'Redirect Test2 Worse Result',
343 ],
344 ] ],
345 [ [
346 "Exact match should override any already found matches that " .
347 "are redirects to it",
348 'provision' => [
349 // Another redirect to the same target as the exact match
350 // is low in the list
351 'Redirect Test Worse Result',
352 'Redirect test',
353 ],
354 'query' => 'Redirect Test',
355 'results' => [
356 // Found redirect is pulled to the top and exact match isn't
357 // added
358 'Redirect Test',
359 'Redirect Test Worse Result',
360 ],
361 ] ],
362 ];
363 }
364
365 /**
366 * @dataProvider provideSearchBackend
367 * @covers PrefixSearch::searchBackend
368 */
369 public function testSearchBackend( array $case ) {
370 $this->searchProvision( $case['provision'] );
371 $searcher = new StringPrefixSearch;
372 $results = $searcher->search( $case['query'], 3 );
373 $this->assertEquals(
374 $case['results'],
375 $results,
376 $case[0]
377 );
378 }
379 }