7 * @covers SearchEngine<extended>
8 * @note Coverage will only ever show one of on of the Search* classes
10 class SearchEngineTest
extends MediaWikiLangTestCase
{
18 * Checks for database type & version.
19 * Will skip current test if DB does not support search.
21 protected function setUp() {
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' );
29 if ( !$dbSupported ) {
30 $this->markTestSkipped( "MySQL or SQLite with FTS3 only" );
33 $searchType = SearchEngineFactory
::getSearchEngineClass( $this->db
);
34 $this->setMwGlobals( [
35 'wgSearchType' => $searchType
38 $this->search
= new $searchType( $this->db
);
41 protected function tearDown() {
42 unset( $this->search
);
47 public function addDBDataOnce() {
48 if ( !$this->isWikitextNS( NS_MAIN
) ) {
49 // @todo cover the case of non-wikitext content in the main namespace
53 // Reset the search type back to default - some extensions may have
55 $this->setMwGlobals( [ 'wgSearchType' => null ] );
57 $this->insertPage( 'Not_Main_Page', 'This is not a main page' );
60 'This is not a talk page to the main page, see [[smithee]]'
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' );
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" );
84 $this->assertTrue( is_object( $results ) );
87 $row = $results->next();
89 $matches[] = $row->getTitle()->getPrefixedText();
90 $row = $results->next();
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.
101 public function testFullWidth() {
103 [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
104 $this->fetchIds( $this->search
->searchText( 'AZ' ) ),
105 "Search for normalized from Half-width Upper" );
107 [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
108 $this->fetchIds( $this->search
->searchText( 'az' ) ),
109 "Search for normalized from Half-width Lower" );
111 [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
112 $this->fetchIds( $this->search
->searchText( 'AZ' ) ),
113 "Search for normalized from Full-width Upper" );
115 [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
116 $this->fetchIds( $this->search
->searchText( 'az' ) ),
117 "Search for normalized from Full-width Lower" );
120 public function testTextSearch() {
123 $this->fetchIds( $this->search
->searchText( 'smithee' ) ),
127 public function testWildcardSearch() {
128 $res = $this->search
->searchText( 'smith*' );
131 $this->fetchIds( $res ),
132 "Search with wildcards" );
134 $res = $this->search
->searchText( 'smithson*' );
137 $this->fetchIds( $res ),
138 "Search with wildcards must not find unrelated articles" );
140 $res = $this->search
->searchText( 'smith* smithee' );
143 $this->fetchIds( $res ),
144 "Search with wildcards can be combined with simple terms" );
146 $res = $this->search
->searchText( 'smith* "one who smiths"' );
149 $this->fetchIds( $res ),
150 "Search with wildcards can be combined with phrase search" );
153 public function testPhraseSearch() {
154 $res = $this->search
->searchText( '"smithee is one who smiths"' );
157 $this->fetchIds( $res ),
160 $res = $this->search
->searchText( '"smithee is who smiths"' );
163 $this->fetchIds( $res ),
164 "Phrase search is not sloppy, search terms must be adjacent" );
166 $res = $this->search
->searchText( '"is smithee one who smiths"' );
169 $this->fetchIds( $res ),
170 "Phrase search is ordered" );
173 public function testPhraseSearchHighlight() {
174 $phrase = "smithee is one who smiths";
175 $res = $this->search
->searchText( "\"$phrase\"" );
176 $match = $res->next();
177 $snippet = "A <span class='searchmatch'>" . $phrase . "</span>";
178 $this->assertStringStartsWith( $snippet,
179 $match->getTextSnippet( $res->termMatches() ),
180 "Highlight a phrase search" );
183 public function testTextPowerSearch() {
184 $this->search
->setNamespaces( [ 0, 1, 4 ] );
188 'Talk:Not Main Page',
190 $this->fetchIds( $this->search
->searchText( 'smithee' ) ),
194 public function testTitleSearch() {
200 $this->fetchIds( $this->search
->searchTitle( 'smithee' ) ),
204 public function testTextTitlePowerSearch() {
205 $this->search
->setNamespaces( [ 0, 1, 4 ] );
212 $this->fetchIds( $this->search
->searchTitle( 'smithee' ) ),
213 "Title power search" );
217 * @covers SearchEngine::getSearchIndexFields
219 public function testSearchIndexFields() {
221 * @var $mockEngine SearchEngine
223 $mockEngine = $this->getMockBuilder( 'SearchEngine' )
224 ->setMethods( [ 'makeSearchFieldMapping' ] )->getMock();
226 $mockFieldBuilder = function ( $name, $type ) {
228 $this->getMockBuilder( 'SearchIndexFieldDefinition' )->setConstructorArgs( [
233 $mockField->expects( $this->any() )->method( 'getMapping' )->willReturn( [
234 'testData' => 'test',
239 $mockField->expects( $this->any() )
241 ->willReturn( $mockField );
246 $mockEngine->expects( $this->atLeastOnce() )
247 ->method( 'makeSearchFieldMapping' )
248 ->willReturnCallback( $mockFieldBuilder );
250 // Not using mock since PHPUnit mocks do not work properly with references in params
251 $this->setTemporaryHook( 'SearchIndexFields',
252 function ( &$fields, SearchEngine
$engine ) use ( $mockFieldBuilder ) {
253 $fields['testField'] =
254 $mockFieldBuilder( "testField", SearchIndexField
::INDEX_TYPE_TEXT
);
258 $fields = $mockEngine->getSearchIndexFields();
259 $this->assertArrayHasKey( 'language', $fields );
260 $this->assertArrayHasKey( 'category', $fields );
261 $this->assertInstanceOf( 'SearchIndexField', $fields['testField'] );
263 $mapping = $fields['testField']->getMapping( $mockEngine );
264 $this->assertArrayHasKey( 'testData', $mapping );
265 $this->assertEquals( 'test', $mapping['testData'] );
268 public function hookSearchIndexFields( $mockFieldBuilder, &$fields, SearchEngine
$engine ) {
269 $fields['testField'] = $mockFieldBuilder( "testField", SearchIndexField
::INDEX_TYPE_TEXT
);
273 public function testAugmentorSearch() {
274 $this->search
->setNamespaces( [ 0, 1, 4 ] );
275 $resultSet = $this->search
->searchText( 'smithee' );
276 // Not using mock since PHPUnit mocks do not work properly with references in params
277 $this->mergeMwGlobalArrayValue( 'wgHooks',
278 [ 'SearchResultsAugment' => [ [ $this, 'addAugmentors' ] ] ] );
279 $this->search
->augmentSearchResults( $resultSet );
280 for ( $result = $resultSet->next(); $result; $result = $resultSet->next() ) {
281 $id = $result->getTitle()->getArticleID();
282 $augmentData = "Result:$id:" . $result->getTitle()->getText();
283 $augmentData2 = "Result2:$id:" . $result->getTitle()->getText();
284 $this->assertEquals( [ 'testSet' => $augmentData, 'testRow' => $augmentData2 ],
285 $result->getExtensionData() );
289 public function addAugmentors( &$setAugmentors, &$rowAugmentors ) {
290 $setAugmentor = $this->createMock( 'ResultSetAugmentor' );
291 $setAugmentor->expects( $this->once() )
292 ->method( 'augmentAll' )
293 ->willReturnCallback( function ( SearchResultSet
$resultSet ) {
295 for ( $result = $resultSet->next(); $result; $result = $resultSet->next() ) {
296 $id = $result->getTitle()->getArticleID();
297 $data[$id] = "Result:$id:" . $result->getTitle()->getText();
299 $resultSet->rewind();
302 $setAugmentors['testSet'] = $setAugmentor;
304 $rowAugmentor = $this->createMock( 'ResultAugmentor' );
305 $rowAugmentor->expects( $this->exactly( 2 ) )
306 ->method( 'augment' )
307 ->willReturnCallback( function ( SearchResult
$result ) {
308 $id = $result->getTitle()->getArticleID();
309 return "Result2:$id:" . $result->getTitle()->getText();
311 $rowAugmentors['testRow'] = $rowAugmentor;