summaryrefslogtreecommitdiff
path: root/www/wiki/tests/phpunit/includes/search/SearchEngineTest.php
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/tests/phpunit/includes/search/SearchEngineTest.php')
-rw-r--r--www/wiki/tests/phpunit/includes/search/SearchEngineTest.php368
1 files changed, 368 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/search/SearchEngineTest.php b/www/wiki/tests/phpunit/includes/search/SearchEngineTest.php
new file mode 100644
index 00000000..b7bc1530
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/search/SearchEngineTest.php
@@ -0,0 +1,368 @@
+<?php
+
+/**
+ * @group Search
+ * @group Database
+ *
+ * @covers SearchEngine<extended>
+ * @note Coverage will only ever show one of on of the Search* classes
+ */
+class SearchEngineTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var SearchEngine
+ */
+ protected $search;
+
+ /**
+ * Checks for database type & version.
+ * Will skip current test if DB does not support search.
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ // Search tests require MySQL or SQLite with FTS
+ $dbType = $this->db->getType();
+ $dbSupported = ( $dbType === 'mysql' )
+ || ( $dbType === 'sqlite' && $this->db->getFulltextSearchModule() == 'FTS3' );
+
+ if ( !$dbSupported ) {
+ $this->markTestSkipped( "MySQL or SQLite with FTS3 only" );
+ }
+
+ $searchType = SearchEngineFactory::getSearchEngineClass( $this->db );
+ $this->setMwGlobals( [
+ 'wgSearchType' => $searchType,
+ 'wgCapitalLinks' => true,
+ 'wgCapitalLinkOverrides' => [
+ NS_CATEGORY => false // for testCompletionSearchMustRespectCapitalLinkOverrides
+ ]
+ ] );
+
+ $this->search = new $searchType( $this->db );
+ }
+
+ protected function tearDown() {
+ unset( $this->search );
+
+ parent::tearDown();
+ }
+
+ public function addDBDataOnce() {
+ if ( !$this->isWikitextNS( NS_MAIN ) ) {
+ // @todo cover the case of non-wikitext content in the main namespace
+ return;
+ }
+
+ // Reset the search type back to default - some extensions may have
+ // overridden it.
+ $this->setMwGlobals( [
+ 'wgSearchType' => null,
+ 'wgCapitalLinks' => true,
+ 'wgCapitalLinkOverrides' => [
+ NS_CATEGORY => false // for testCompletionSearchMustRespectCapitalLinkOverrides
+ ]
+ ] );
+
+ $this->insertPage( 'Not_Main_Page', 'This is not a main page' );
+ $this->insertPage(
+ 'Talk:Not_Main_Page',
+ 'This is not a talk page to the main page, see [[smithee]]'
+ );
+ $this->insertPage( 'Smithee', 'A smithee is one who smiths. See also [[Alan Smithee]]' );
+ $this->insertPage( 'Talk:Smithee', 'This article sucks.' );
+ $this->insertPage( 'Unrelated_page', 'Nothing in this page is about the S word.' );
+ $this->insertPage( 'Another_page', 'This page also is unrelated.' );
+ $this->insertPage( 'Help:Help', 'Help me!' );
+ $this->insertPage( 'Thppt', 'Blah blah' );
+ $this->insertPage( 'Alan_Smithee', 'yum' );
+ $this->insertPage( 'Pages', 'are\'food' );
+ $this->insertPage( 'HalfOneUp', 'AZ' );
+ $this->insertPage( 'FullOneUp', 'AZ' );
+ $this->insertPage( 'HalfTwoLow', 'az' );
+ $this->insertPage( 'FullTwoLow', 'az' );
+ $this->insertPage( 'HalfNumbers', '1234567890' );
+ $this->insertPage( 'FullNumbers', '1234567890' );
+ $this->insertPage( 'DomainName', 'example.com' );
+ $this->insertPage( 'DomainName', 'example.com' );
+ $this->insertPage( 'Category:search is not Search', '' );
+ $this->insertPage( 'Category:Search is not search', '' );
+ }
+
+ protected function fetchIds( $results ) {
+ if ( !$this->isWikitextNS( NS_MAIN ) ) {
+ $this->markTestIncomplete( __CLASS__ . " does no yet support non-wikitext content "
+ . "in the main namespace" );
+ }
+ $this->assertTrue( is_object( $results ) );
+
+ $matches = [];
+ $row = $results->next();
+ while ( $row ) {
+ $matches[] = $row->getTitle()->getPrefixedText();
+ $row = $results->next();
+ }
+ $results->free();
+ # Search is not guaranteed to return results in a certain order;
+ # sort them numerically so we will compare simply that we received
+ # the expected matches.
+ sort( $matches );
+
+ return $matches;
+ }
+
+ public function testFullWidth() {
+ $this->assertEquals(
+ [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
+ $this->fetchIds( $this->search->searchText( 'AZ' ) ),
+ "Search for normalized from Half-width Upper" );
+ $this->assertEquals(
+ [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
+ $this->fetchIds( $this->search->searchText( 'az' ) ),
+ "Search for normalized from Half-width Lower" );
+ $this->assertEquals(
+ [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
+ $this->fetchIds( $this->search->searchText( 'AZ' ) ),
+ "Search for normalized from Full-width Upper" );
+ $this->assertEquals(
+ [ 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ],
+ $this->fetchIds( $this->search->searchText( 'az' ) ),
+ "Search for normalized from Full-width Lower" );
+ }
+
+ public function testTextSearch() {
+ $this->assertEquals(
+ [ 'Smithee' ],
+ $this->fetchIds( $this->search->searchText( 'smithee' ) ),
+ "Plain search" );
+ }
+
+ public function testWildcardSearch() {
+ $res = $this->search->searchText( 'smith*' );
+ $this->assertEquals(
+ [ 'Smithee' ],
+ $this->fetchIds( $res ),
+ "Search with wildcards" );
+
+ $res = $this->search->searchText( 'smithson*' );
+ $this->assertEquals(
+ [],
+ $this->fetchIds( $res ),
+ "Search with wildcards must not find unrelated articles" );
+
+ $res = $this->search->searchText( 'smith* smithee' );
+ $this->assertEquals(
+ [ 'Smithee' ],
+ $this->fetchIds( $res ),
+ "Search with wildcards can be combined with simple terms" );
+
+ $res = $this->search->searchText( 'smith* "one who smiths"' );
+ $this->assertEquals(
+ [ 'Smithee' ],
+ $this->fetchIds( $res ),
+ "Search with wildcards can be combined with phrase search" );
+ }
+
+ public function testPhraseSearch() {
+ $res = $this->search->searchText( '"smithee is one who smiths"' );
+ $this->assertEquals(
+ [ 'Smithee' ],
+ $this->fetchIds( $res ),
+ "Search a phrase" );
+
+ $res = $this->search->searchText( '"smithee is who smiths"' );
+ $this->assertEquals(
+ [],
+ $this->fetchIds( $res ),
+ "Phrase search is not sloppy, search terms must be adjacent" );
+
+ $res = $this->search->searchText( '"is smithee one who smiths"' );
+ $this->assertEquals(
+ [],
+ $this->fetchIds( $res ),
+ "Phrase search is ordered" );
+ }
+
+ public function testPhraseSearchHighlight() {
+ $phrase = "smithee is one who smiths";
+ $res = $this->search->searchText( "\"$phrase\"" );
+ $match = $res->next();
+ $snippet = "A <span class='searchmatch'>" . $phrase . "</span>";
+ $this->assertStringStartsWith( $snippet,
+ $match->getTextSnippet( $res->termMatches() ),
+ "Highlight a phrase search" );
+ }
+
+ public function testTextPowerSearch() {
+ $this->search->setNamespaces( [ 0, 1, 4 ] );
+ $this->assertEquals(
+ [
+ 'Smithee',
+ 'Talk:Not Main Page',
+ ],
+ $this->fetchIds( $this->search->searchText( 'smithee' ) ),
+ "Power search" );
+ }
+
+ public function testTitleSearch() {
+ $this->assertEquals(
+ [
+ 'Alan Smithee',
+ 'Smithee',
+ ],
+ $this->fetchIds( $this->search->searchTitle( 'smithee' ) ),
+ "Title search" );
+ }
+
+ public function testTextTitlePowerSearch() {
+ $this->search->setNamespaces( [ 0, 1, 4 ] );
+ $this->assertEquals(
+ [
+ 'Alan Smithee',
+ 'Smithee',
+ 'Talk:Smithee',
+ ],
+ $this->fetchIds( $this->search->searchTitle( 'smithee' ) ),
+ "Title power search" );
+ }
+
+ public function provideCompletionSearchMustRespectCapitalLinkOverrides() {
+ return [
+ 'Searching for "smithee" finds Smithee on NS_MAIN' => [
+ 'smithee',
+ 'Smithee',
+ [ NS_MAIN ],
+ ],
+ 'Searching for "search is" will finds "search is not Search" on NS_CATEGORY' => [
+ 'search is',
+ 'Category:search is not Search',
+ [ NS_CATEGORY ],
+ ],
+ 'Searching for "Search is" will finds "search is not Search" on NS_CATEGORY' => [
+ 'Search is',
+ 'Category:Search is not search',
+ [ NS_CATEGORY ],
+ ],
+ ];
+ }
+
+ /**
+ * Test that the search query is not munged using wrong CapitalLinks setup
+ * (in other test that the default search backend can benefit from wgCapitalLinksOverride)
+ * Guard against regressions like T208255
+ * @dataProvider provideCompletionSearchMustRespectCapitalLinkOverrides
+ * @covers SearchEngine::completionSearch
+ * @covers PrefixSearch::defaultSearchBackend
+ * @param string $search
+ * @param string $expectedSuggestion
+ * @param int[] $namespaces
+ */
+ public function testCompletionSearchMustRespectCapitalLinkOverrides(
+ $search,
+ $expectedSuggestion,
+ array $namespaces
+ ) {
+ $this->search->setNamespaces( $namespaces );
+ $results = $this->search->completionSearch( $search );
+ $this->assertEquals( 1, $results->getSize() );
+ $this->assertEquals( $expectedSuggestion, $results->getSuggestions()[0]->getText() );
+ }
+
+ /**
+ * @covers SearchEngine::getSearchIndexFields
+ */
+ public function testSearchIndexFields() {
+ /**
+ * @var $mockEngine SearchEngine
+ */
+ $mockEngine = $this->getMockBuilder( SearchEngine::class )
+ ->setMethods( [ 'makeSearchFieldMapping' ] )->getMock();
+
+ $mockFieldBuilder = function ( $name, $type ) {
+ $mockField =
+ $this->getMockBuilder( SearchIndexFieldDefinition::class )->setConstructorArgs( [
+ $name,
+ $type
+ ] )->getMock();
+
+ $mockField->expects( $this->any() )->method( 'getMapping' )->willReturn( [
+ 'testData' => 'test',
+ 'name' => $name,
+ 'type' => $type,
+ ] );
+
+ $mockField->expects( $this->any() )
+ ->method( 'merge' )
+ ->willReturn( $mockField );
+
+ return $mockField;
+ };
+
+ $mockEngine->expects( $this->atLeastOnce() )
+ ->method( 'makeSearchFieldMapping' )
+ ->willReturnCallback( $mockFieldBuilder );
+
+ // Not using mock since PHPUnit mocks do not work properly with references in params
+ $this->setTemporaryHook( 'SearchIndexFields',
+ function ( &$fields, SearchEngine $engine ) use ( $mockFieldBuilder ) {
+ $fields['testField'] =
+ $mockFieldBuilder( "testField", SearchIndexField::INDEX_TYPE_TEXT );
+ return true;
+ } );
+
+ $fields = $mockEngine->getSearchIndexFields();
+ $this->assertArrayHasKey( 'language', $fields );
+ $this->assertArrayHasKey( 'category', $fields );
+ $this->assertInstanceOf( SearchIndexField::class, $fields['testField'] );
+
+ $mapping = $fields['testField']->getMapping( $mockEngine );
+ $this->assertArrayHasKey( 'testData', $mapping );
+ $this->assertEquals( 'test', $mapping['testData'] );
+ }
+
+ public function hookSearchIndexFields( $mockFieldBuilder, &$fields, SearchEngine $engine ) {
+ $fields['testField'] = $mockFieldBuilder( "testField", SearchIndexField::INDEX_TYPE_TEXT );
+ return true;
+ }
+
+ public function testAugmentorSearch() {
+ $this->search->setNamespaces( [ 0, 1, 4 ] );
+ $resultSet = $this->search->searchText( 'smithee' );
+ // Not using mock since PHPUnit mocks do not work properly with references in params
+ $this->mergeMwGlobalArrayValue( 'wgHooks',
+ [ 'SearchResultsAugment' => [ [ $this, 'addAugmentors' ] ] ] );
+ $this->search->augmentSearchResults( $resultSet );
+ for ( $result = $resultSet->next(); $result; $result = $resultSet->next() ) {
+ $id = $result->getTitle()->getArticleID();
+ $augmentData = "Result:$id:" . $result->getTitle()->getText();
+ $augmentData2 = "Result2:$id:" . $result->getTitle()->getText();
+ $this->assertEquals( [ 'testSet' => $augmentData, 'testRow' => $augmentData2 ],
+ $result->getExtensionData() );
+ }
+ }
+
+ public function addAugmentors( &$setAugmentors, &$rowAugmentors ) {
+ $setAugmentor = $this->createMock( ResultSetAugmentor::class );
+ $setAugmentor->expects( $this->once() )
+ ->method( 'augmentAll' )
+ ->willReturnCallback( function ( SearchResultSet $resultSet ) {
+ $data = [];
+ for ( $result = $resultSet->next(); $result; $result = $resultSet->next() ) {
+ $id = $result->getTitle()->getArticleID();
+ $data[$id] = "Result:$id:" . $result->getTitle()->getText();
+ }
+ $resultSet->rewind();
+ return $data;
+ } );
+ $setAugmentors['testSet'] = $setAugmentor;
+
+ $rowAugmentor = $this->createMock( ResultAugmentor::class );
+ $rowAugmentor->expects( $this->exactly( 2 ) )
+ ->method( 'augment' )
+ ->willReturnCallback( function ( SearchResult $result ) {
+ $id = $result->getTitle()->getArticleID();
+ return "Result2:$id:" . $result->getTitle()->getText();
+ } );
+ $rowAugmentors['testRow'] = $rowAugmentor;
+ }
+}