Merge "Fix \n handling for HTMLUsersMultiselectField"
[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 ] );
37
38 $this->search = new $searchType( $this->db );
39 }
40
41 protected function tearDown() {
42 unset( $this->search );
43
44 parent::tearDown();
45 }
46
47 public function addDBDataOnce() {
48 if ( !$this->isWikitextNS( NS_MAIN ) ) {
49 // @todo cover the case of non-wikitext content in the main namespace
50 return;
51 }
52
53 // Reset the search type back to default - some extensions may have
54 // overridden it.
55 $this->setMwGlobals( [ 'wgSearchType' => null ] );
56
57 $this->insertPage( 'Not_Main_Page', 'This is not a main page' );
58 $this->insertPage(
59 'Talk:Not_Main_Page',
60 'This is not a talk page to the main page, see [[smithee]]'
61 );
62 $this->insertPage( 'Smithee', 'A smithee is one who smiths. See also [[Alan Smithee]]' );
63 $this->insertPage( 'Talk:Smithee', 'This article sucks.' );
64 $this->insertPage( 'Unrelated_page', 'Nothing in this page is about the S word.' );
65 $this->insertPage( 'Another_page', 'This page also is unrelated.' );
66 $this->insertPage( 'Help:Help', 'Help me!' );
67 $this->insertPage( 'Thppt', 'Blah blah' );
68 $this->insertPage( 'Alan_Smithee', 'yum' );
69 $this->insertPage( 'Pages', 'are\'food' );
70 $this->insertPage( 'HalfOneUp', 'AZ' );
71 $this->insertPage( 'FullOneUp', 'AZ' );
72 $this->insertPage( 'HalfTwoLow', 'az' );
73 $this->insertPage( 'FullTwoLow', 'az' );
74 $this->insertPage( 'HalfNumbers', '1234567890' );
75 $this->insertPage( 'FullNumbers', '1234567890' );
76 $this->insertPage( 'DomainName', 'example.com' );
77 }
78
79 protected function fetchIds( $results ) {
80 if ( !$this->isWikitextNS( NS_MAIN ) ) {
81 $this->markTestIncomplete( __CLASS__ . " does no yet support non-wikitext content "
82 . "in the main namespace" );
83 }
84 $this->assertTrue( is_object( $results ) );
85
86 $matches = [];
87 $row = $results->next();
88 while ( $row ) {
89 $matches[] = $row->getTitle()->getPrefixedText();
90 $row = $results->next();
91 }
92 $results->free();
93 # Search is not guaranteed to return results in a certain order;
94 # sort them numerically so we will compare simply that we received
95 # the expected matches.
96 sort( $matches );
97
98 return $matches;
99 }
100
101 public function testFullWidth() {
102 $this->assertEquals(
103 [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
104 $this->fetchIds( $this->search->searchText( 'AZ' ) ),
105 "Search for normalized from Half-width Upper" );
106 $this->assertEquals(
107 [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
108 $this->fetchIds( $this->search->searchText( 'az' ) ),
109 "Search for normalized from Half-width Lower" );
110 $this->assertEquals(
111 [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
112 $this->fetchIds( $this->search->searchText( 'AZ' ) ),
113 "Search for normalized from Full-width Upper" );
114 $this->assertEquals(
115 [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
116 $this->fetchIds( $this->search->searchText( 'az' ) ),
117 "Search for normalized from Full-width Lower" );
118 }
119
120 public function testTextSearch() {
121 $this->assertEquals(
122 [ 'Smithee' ],
123 $this->fetchIds( $this->search->searchText( 'smithee' ) ),
124 "Plain search" );
125 }
126
127 public function testPhraseSearch() {
128 $res = $this->search->searchText( '"smithee is one who smiths"' );
129 $this->assertEquals(
130 [ 'Smithee' ],
131 $this->fetchIds( $res ),
132 "Search a phrase" );
133 $res = $this->search->searchText( '"smithee is one who smiths"' );
134 $match = $res->next();
135 $terms = [ 'smithee', 'is', 'one', 'who', 'smiths' ];
136 $snippet = "";
137 foreach ( $terms as $term ) {
138 $snippet .= " <span class='searchmatch'>" . $term . "</span>";
139 }
140 $this->assertRegexp( '/' . preg_quote( $snippet, '/' ) . '/',
141 $match->getTextSnippet( $res->termMatches() ),
142 "Highlight a phrase search" );
143 }
144
145 public function testTextPowerSearch() {
146 $this->search->setNamespaces( [ 0, 1, 4 ] );
147 $this->assertEquals(
148 [
149 'Smithee',
150 'Talk:Not Main Page',
151 ],
152 $this->fetchIds( $this->search->searchText( 'smithee' ) ),
153 "Power search" );
154 }
155
156 public function testTitleSearch() {
157 $this->assertEquals(
158 [
159 'Alan Smithee',
160 'Smithee',
161 ],
162 $this->fetchIds( $this->search->searchTitle( 'smithee' ) ),
163 "Title search" );
164 }
165
166 public function testTextTitlePowerSearch() {
167 $this->search->setNamespaces( [ 0, 1, 4 ] );
168 $this->assertEquals(
169 [
170 'Alan Smithee',
171 'Smithee',
172 'Talk:Smithee',
173 ],
174 $this->fetchIds( $this->search->searchTitle( 'smithee' ) ),
175 "Title power search" );
176 }
177
178 /**
179 * @covers SearchEngine::getSearchIndexFields
180 */
181 public function testSearchIndexFields() {
182 /**
183 * @var $mockEngine SearchEngine
184 */
185 $mockEngine = $this->getMockBuilder( 'SearchEngine' )
186 ->setMethods( [ 'makeSearchFieldMapping' ] )->getMock();
187
188 $mockFieldBuilder = function ( $name, $type ) {
189 $mockField =
190 $this->getMockBuilder( 'SearchIndexFieldDefinition' )->setConstructorArgs( [
191 $name,
192 $type
193 ] )->getMock();
194
195 $mockField->expects( $this->any() )->method( 'getMapping' )->willReturn( [
196 'testData' => 'test',
197 'name' => $name,
198 'type' => $type,
199 ] );
200
201 $mockField->expects( $this->any() )
202 ->method( 'merge' )
203 ->willReturn( $mockField );
204
205 return $mockField;
206 };
207
208 $mockEngine->expects( $this->atLeastOnce() )
209 ->method( 'makeSearchFieldMapping' )
210 ->willReturnCallback( $mockFieldBuilder );
211
212 // Not using mock since PHPUnit mocks do not work properly with references in params
213 $this->setTemporaryHook( 'SearchIndexFields',
214 function ( &$fields, SearchEngine $engine ) use ( $mockFieldBuilder ) {
215 $fields['testField'] =
216 $mockFieldBuilder( "testField", SearchIndexField::INDEX_TYPE_TEXT );
217 return true;
218 } );
219
220 $fields = $mockEngine->getSearchIndexFields();
221 $this->assertArrayHasKey( 'language', $fields );
222 $this->assertArrayHasKey( 'category', $fields );
223 $this->assertInstanceOf( 'SearchIndexField', $fields['testField'] );
224
225 $mapping = $fields['testField']->getMapping( $mockEngine );
226 $this->assertArrayHasKey( 'testData', $mapping );
227 $this->assertEquals( 'test', $mapping['testData'] );
228 }
229
230 public function hookSearchIndexFields( $mockFieldBuilder, &$fields, SearchEngine $engine ) {
231 $fields['testField'] = $mockFieldBuilder( "testField", SearchIndexField::INDEX_TYPE_TEXT );
232 return true;
233 }
234
235 public function testAugmentorSearch() {
236 $this->search->setNamespaces( [ 0, 1, 4 ] );
237 $resultSet = $this->search->searchText( 'smithee' );
238 // Not using mock since PHPUnit mocks do not work properly with references in params
239 $this->mergeMwGlobalArrayValue( 'wgHooks',
240 [ 'SearchResultsAugment' => [ [ $this, 'addAugmentors' ] ] ] );
241 $this->search->augmentSearchResults( $resultSet );
242 for ( $result = $resultSet->next(); $result; $result = $resultSet->next() ) {
243 $id = $result->getTitle()->getArticleID();
244 $augmentData = "Result:$id:" . $result->getTitle()->getText();
245 $augmentData2 = "Result2:$id:" . $result->getTitle()->getText();
246 $this->assertEquals( [ 'testSet' => $augmentData, 'testRow' => $augmentData2 ],
247 $result->getExtensionData() );
248 }
249 }
250
251 public function addAugmentors( &$setAugmentors, &$rowAugmentors ) {
252 $setAugmentor = $this->createMock( 'ResultSetAugmentor' );
253 $setAugmentor->expects( $this->once() )
254 ->method( 'augmentAll' )
255 ->willReturnCallback( function ( SearchResultSet $resultSet ) {
256 $data = [];
257 for ( $result = $resultSet->next(); $result; $result = $resultSet->next() ) {
258 $id = $result->getTitle()->getArticleID();
259 $data[$id] = "Result:$id:" . $result->getTitle()->getText();
260 }
261 $resultSet->rewind();
262 return $data;
263 } );
264 $setAugmentors['testSet'] = $setAugmentor;
265
266 $rowAugmentor = $this->createMock( 'ResultAugmentor' );
267 $rowAugmentor->expects( $this->exactly( 2 ) )
268 ->method( 'augment' )
269 ->willReturnCallback( function ( SearchResult $result ) {
270 $id = $result->getTitle()->getArticleID();
271 return "Result2:$id:" . $result->getTitle()->getText();
272 } );
273 $rowAugmentors['testRow'] = $rowAugmentor;
274 }
275 }