Add test for completionSearch with wgCapitalLinkOverrides
[lhc/web/wiklou.git] / tests / phpunit / includes / search / SearchEngineTest.php
1 <?php
2
3 /**
4 * @group Search
5 * @group Database
6 *
7 * @covers SearchEngine<extended>
8 * @note Coverage will only ever show one of on of the Search* classes
9 */
10 class SearchEngineTest extends MediaWikiLangTestCase {
11
12 /**
13 * @var SearchEngine
14 */
15 protected $search;
16
17 /**
18 * Checks for database type & version.
19 * Will skip current test if DB does not support search.
20 */
21 protected function setUp() {
22 parent::setUp();
23
24 // Search tests require MySQL or SQLite with FTS
25 $dbType = $this->db->getType();
26 $dbSupported = ( $dbType === 'mysql' )
27 || ( $dbType === 'sqlite' && $this->db->getFulltextSearchModule() == 'FTS3' );
28
29 if ( !$dbSupported ) {
30 $this->markTestSkipped( "MySQL or SQLite with FTS3 only" );
31 }
32
33 $searchType = SearchEngineFactory::getSearchEngineClass( $this->db );
34 $this->setMwGlobals( [
35 'wgSearchType' => $searchType,
36 'wgCapitalLinks' => true,
37 'wgCapitalLinkOverrides' => [
38 NS_CATEGORY => false // for testCompletionSearchMustRespectCapitalLinkOverrides
39 ]
40 ] );
41
42 $this->search = new $searchType( $this->db );
43 }
44
45 protected function tearDown() {
46 unset( $this->search );
47
48 parent::tearDown();
49 }
50
51 public function addDBDataOnce() {
52 if ( !$this->isWikitextNS( NS_MAIN ) ) {
53 // @todo cover the case of non-wikitext content in the main namespace
54 return;
55 }
56
57 // Reset the search type back to default - some extensions may have
58 // overridden it.
59 $this->setMwGlobals( [
60 'wgSearchType' => null,
61 'wgCapitalLinks' => true,
62 'wgCapitalLinkOverrides' => [
63 NS_CATEGORY => false // for testCompletionSearchMustRespectCapitalLinkOverrides
64 ]
65 ] );
66
67 $this->insertPage( 'Not_Main_Page', 'This is not a main page' );
68 $this->insertPage(
69 'Talk:Not_Main_Page',
70 'This is not a talk page to the main page, see [[smithee]]'
71 );
72 $this->insertPage( 'Smithee', 'A smithee is one who smiths. See also [[Alan Smithee]]' );
73 $this->insertPage( 'Talk:Smithee', 'This article sucks.' );
74 $this->insertPage( 'Unrelated_page', 'Nothing in this page is about the S word.' );
75 $this->insertPage( 'Another_page', 'This page also is unrelated.' );
76 $this->insertPage( 'Help:Help', 'Help me!' );
77 $this->insertPage( 'Thppt', 'Blah blah' );
78 $this->insertPage( 'Alan_Smithee', 'yum' );
79 $this->insertPage( 'Pages', 'are\'food' );
80 $this->insertPage( 'HalfOneUp', 'AZ' );
81 $this->insertPage( 'FullOneUp', 'AZ' );
82 $this->insertPage( 'HalfTwoLow', 'az' );
83 $this->insertPage( 'FullTwoLow', 'az' );
84 $this->insertPage( 'HalfNumbers', '1234567890' );
85 $this->insertPage( 'FullNumbers', '1234567890' );
86 $this->insertPage( 'DomainName', 'example.com' );
87 $this->insertPage( 'DomainName', 'example.com' );
88 $this->insertPage( 'Category:search is not Search', '' );
89 $this->insertPage( 'Category:Search is not search', '' );
90 }
91
92 protected function fetchIds( $results ) {
93 if ( !$this->isWikitextNS( NS_MAIN ) ) {
94 $this->markTestIncomplete( __CLASS__ . " does no yet support non-wikitext content "
95 . "in the main namespace" );
96 }
97 $this->assertTrue( is_object( $results ) );
98
99 $matches = [];
100 $row = $results->next();
101 while ( $row ) {
102 $matches[] = $row->getTitle()->getPrefixedText();
103 $row = $results->next();
104 }
105 $results->free();
106 # Search is not guaranteed to return results in a certain order;
107 # sort them numerically so we will compare simply that we received
108 # the expected matches.
109 sort( $matches );
110
111 return $matches;
112 }
113
114 public function testFullWidth() {
115 $this->assertEquals(
116 [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
117 $this->fetchIds( $this->search->searchText( 'AZ' ) ),
118 "Search for normalized from Half-width Upper" );
119 $this->assertEquals(
120 [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
121 $this->fetchIds( $this->search->searchText( 'az' ) ),
122 "Search for normalized from Half-width Lower" );
123 $this->assertEquals(
124 [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
125 $this->fetchIds( $this->search->searchText( 'AZ' ) ),
126 "Search for normalized from Full-width Upper" );
127 $this->assertEquals(
128 [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
129 $this->fetchIds( $this->search->searchText( 'az' ) ),
130 "Search for normalized from Full-width Lower" );
131 }
132
133 public function testTextSearch() {
134 $this->assertEquals(
135 [ 'Smithee' ],
136 $this->fetchIds( $this->search->searchText( 'smithee' ) ),
137 "Plain search" );
138 }
139
140 public function testWildcardSearch() {
141 $res = $this->search->searchText( 'smith*' );
142 $this->assertEquals(
143 [ 'Smithee' ],
144 $this->fetchIds( $res ),
145 "Search with wildcards" );
146
147 $res = $this->search->searchText( 'smithson*' );
148 $this->assertEquals(
149 [],
150 $this->fetchIds( $res ),
151 "Search with wildcards must not find unrelated articles" );
152
153 $res = $this->search->searchText( 'smith* smithee' );
154 $this->assertEquals(
155 [ 'Smithee' ],
156 $this->fetchIds( $res ),
157 "Search with wildcards can be combined with simple terms" );
158
159 $res = $this->search->searchText( 'smith* "one who smiths"' );
160 $this->assertEquals(
161 [ 'Smithee' ],
162 $this->fetchIds( $res ),
163 "Search with wildcards can be combined with phrase search" );
164 }
165
166 public function testPhraseSearch() {
167 $res = $this->search->searchText( '"smithee is one who smiths"' );
168 $this->assertEquals(
169 [ 'Smithee' ],
170 $this->fetchIds( $res ),
171 "Search a phrase" );
172
173 $res = $this->search->searchText( '"smithee is who smiths"' );
174 $this->assertEquals(
175 [],
176 $this->fetchIds( $res ),
177 "Phrase search is not sloppy, search terms must be adjacent" );
178
179 $res = $this->search->searchText( '"is smithee one who smiths"' );
180 $this->assertEquals(
181 [],
182 $this->fetchIds( $res ),
183 "Phrase search is ordered" );
184 }
185
186 public function testPhraseSearchHighlight() {
187 $phrase = "smithee is one who smiths";
188 $res = $this->search->searchText( "\"$phrase\"" );
189 $match = $res->next();
190 $snippet = "A <span class='searchmatch'>" . $phrase . "</span>";
191 $this->assertStringStartsWith( $snippet,
192 $match->getTextSnippet( $res->termMatches() ),
193 "Highlight a phrase search" );
194 }
195
196 public function testTextPowerSearch() {
197 $this->search->setNamespaces( [ 0, 1, 4 ] );
198 $this->assertEquals(
199 [
200 'Smithee',
201 'Talk:Not Main Page',
202 ],
203 $this->fetchIds( $this->search->searchText( 'smithee' ) ),
204 "Power search" );
205 }
206
207 public function testTitleSearch() {
208 $this->assertEquals(
209 [
210 'Alan Smithee',
211 'Smithee',
212 ],
213 $this->fetchIds( $this->search->searchTitle( 'smithee' ) ),
214 "Title search" );
215 }
216
217 public function testTextTitlePowerSearch() {
218 $this->search->setNamespaces( [ 0, 1, 4 ] );
219 $this->assertEquals(
220 [
221 'Alan Smithee',
222 'Smithee',
223 'Talk:Smithee',
224 ],
225 $this->fetchIds( $this->search->searchTitle( 'smithee' ) ),
226 "Title power search" );
227 }
228
229 public function provideCompletionSearchMustRespectCapitalLinkOverrides() {
230 return [
231 'Searching for "smithee" finds Smithee on NS_MAIN' => [
232 'smithee',
233 'Smithee',
234 [ NS_MAIN ],
235 ],
236 'Searching for "search is" will finds "search is not Search" on NS_CATEGORY' => [
237 'search is',
238 'Category:search is not Search',
239 [ NS_CATEGORY ],
240 ],
241 'Searching for "Search is" will finds "search is not Search" on NS_CATEGORY' => [
242 'Search is',
243 'Category:Search is not search',
244 [ NS_CATEGORY ],
245 ],
246 ];
247 }
248
249 /**
250 * Test that the search query is not munged using wrong CapitalLinks setup
251 * (in other test that the default search backend can benefit from wgCapitalLinksOverride)
252 * Guard against regressions like T208255
253 * @dataProvider provideCompletionSearchMustRespectCapitalLinkOverrides
254 * @covers SearchEngine::completionSearch
255 * @covers PrefixSearch::defaultSearchBackend
256 * @param string $search
257 * @param string $expectedSuggestion
258 * @param int[] $namespaces
259 */
260 public function testCompletionSearchMustRespectCapitalLinkOverrides(
261 $search,
262 $expectedSuggestion,
263 array $namespaces
264 ) {
265 $this->search->setNamespaces( $namespaces );
266 $results = $this->search->completionSearch( $search );
267 $this->assertEquals( 1, $results->getSize() );
268 $this->assertEquals( $expectedSuggestion, $results->getSuggestions()[0]->getText() );
269 }
270
271 /**
272 * @covers SearchEngine::getSearchIndexFields
273 */
274 public function testSearchIndexFields() {
275 /**
276 * @var $mockEngine SearchEngine
277 */
278 $mockEngine = $this->getMockBuilder( SearchEngine::class )
279 ->setMethods( [ 'makeSearchFieldMapping' ] )->getMock();
280
281 $mockFieldBuilder = function ( $name, $type ) {
282 $mockField =
283 $this->getMockBuilder( SearchIndexFieldDefinition::class )->setConstructorArgs( [
284 $name,
285 $type
286 ] )->getMock();
287
288 $mockField->expects( $this->any() )->method( 'getMapping' )->willReturn( [
289 'testData' => 'test',
290 'name' => $name,
291 'type' => $type,
292 ] );
293
294 $mockField->expects( $this->any() )
295 ->method( 'merge' )
296 ->willReturn( $mockField );
297
298 return $mockField;
299 };
300
301 $mockEngine->expects( $this->atLeastOnce() )
302 ->method( 'makeSearchFieldMapping' )
303 ->willReturnCallback( $mockFieldBuilder );
304
305 // Not using mock since PHPUnit mocks do not work properly with references in params
306 $this->setTemporaryHook( 'SearchIndexFields',
307 function ( &$fields, SearchEngine $engine ) use ( $mockFieldBuilder ) {
308 $fields['testField'] =
309 $mockFieldBuilder( "testField", SearchIndexField::INDEX_TYPE_TEXT );
310 return true;
311 } );
312
313 $fields = $mockEngine->getSearchIndexFields();
314 $this->assertArrayHasKey( 'language', $fields );
315 $this->assertArrayHasKey( 'category', $fields );
316 $this->assertInstanceOf( SearchIndexField::class, $fields['testField'] );
317
318 $mapping = $fields['testField']->getMapping( $mockEngine );
319 $this->assertArrayHasKey( 'testData', $mapping );
320 $this->assertEquals( 'test', $mapping['testData'] );
321 }
322
323 public function hookSearchIndexFields( $mockFieldBuilder, &$fields, SearchEngine $engine ) {
324 $fields['testField'] = $mockFieldBuilder( "testField", SearchIndexField::INDEX_TYPE_TEXT );
325 return true;
326 }
327
328 public function testAugmentorSearch() {
329 $this->search->setNamespaces( [ 0, 1, 4 ] );
330 $resultSet = $this->search->searchText( 'smithee' );
331 // Not using mock since PHPUnit mocks do not work properly with references in params
332 $this->mergeMwGlobalArrayValue( 'wgHooks',
333 [ 'SearchResultsAugment' => [ [ $this, 'addAugmentors' ] ] ] );
334 $this->search->augmentSearchResults( $resultSet );
335 for ( $result = $resultSet->next(); $result; $result = $resultSet->next() ) {
336 $id = $result->getTitle()->getArticleID();
337 $augmentData = "Result:$id:" . $result->getTitle()->getText();
338 $augmentData2 = "Result2:$id:" . $result->getTitle()->getText();
339 $this->assertEquals( [ 'testSet' => $augmentData, 'testRow' => $augmentData2 ],
340 $result->getExtensionData() );
341 }
342 }
343
344 public function addAugmentors( &$setAugmentors, &$rowAugmentors ) {
345 $setAugmentor = $this->createMock( ResultSetAugmentor::class );
346 $setAugmentor->expects( $this->once() )
347 ->method( 'augmentAll' )
348 ->willReturnCallback( function ( SearchResultSet $resultSet ) {
349 $data = [];
350 for ( $result = $resultSet->next(); $result; $result = $resultSet->next() ) {
351 $id = $result->getTitle()->getArticleID();
352 $data[$id] = "Result:$id:" . $result->getTitle()->getText();
353 }
354 $resultSet->rewind();
355 return $data;
356 } );
357 $setAugmentors['testSet'] = $setAugmentor;
358
359 $rowAugmentor = $this->createMock( ResultAugmentor::class );
360 $rowAugmentor->expects( $this->exactly( 2 ) )
361 ->method( 'augment' )
362 ->willReturnCallback( function ( SearchResult $result ) {
363 $id = $result->getTitle()->getArticleID();
364 return "Result2:$id:" . $result->getTitle()->getText();
365 } );
366 $rowAugmentors['testRow'] = $rowAugmentor;
367 }
368 }