diff options
Diffstat (limited to 'www/wiki/tests/phpunit/includes/search')
5 files changed, 960 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/search/ParserOutputSearchDataExtractorTest.php b/www/wiki/tests/phpunit/includes/search/ParserOutputSearchDataExtractorTest.php new file mode 100644 index 00000000..69d0b76f --- /dev/null +++ b/www/wiki/tests/phpunit/includes/search/ParserOutputSearchDataExtractorTest.php @@ -0,0 +1,70 @@ +<?php + +use MediaWiki\Search\ParserOutputSearchDataExtractor; + +/** + * @group Search + * @covers MediaWiki\Search\ParserOutputSearchDataExtractor + */ +class ParserOutputSearchDataExtractorTest extends MediaWikiLangTestCase { + + public function testGetCategories() { + $categories = [ + 'Foo_bar' => 'Bar', + 'New_page' => '' + ]; + + $parserOutput = new ParserOutput( '', [], $categories ); + + $searchDataExtractor = new ParserOutputSearchDataExtractor(); + + $this->assertEquals( + [ 'Foo bar', 'New page' ], + $searchDataExtractor->getCategories( $parserOutput ) + ); + } + + public function testGetExternalLinks() { + $parserOutput = new ParserOutput(); + + $parserOutput->addExternalLink( 'https://foo' ); + $parserOutput->addExternalLink( 'https://bar' ); + + $searchDataExtractor = new ParserOutputSearchDataExtractor(); + + $this->assertEquals( + [ 'https://foo', 'https://bar' ], + $searchDataExtractor->getExternalLinks( $parserOutput ) + ); + } + + public function testGetOutgoingLinks() { + $parserOutput = new ParserOutput(); + + $parserOutput->addLink( Title::makeTitle( NS_MAIN, 'Foo_bar' ), 1 ); + $parserOutput->addLink( Title::makeTitle( NS_HELP, 'Contents' ), 2 ); + + $searchDataExtractor = new ParserOutputSearchDataExtractor(); + + // this indexes links with db key + $this->assertEquals( + [ 'Foo_bar', 'Help:Contents' ], + $searchDataExtractor->getOutgoingLinks( $parserOutput ) + ); + } + + public function testGetTemplates() { + $title = Title::makeTitle( NS_TEMPLATE, 'Cite_news' ); + + $parserOutput = new ParserOutput(); + $parserOutput->addTemplate( $title, 10, 100 ); + + $searchDataExtractor = new ParserOutputSearchDataExtractor(); + + $this->assertEquals( + [ 'Template:Cite news' ], + $searchDataExtractor->getTemplates( $parserOutput ) + ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/search/SearchEnginePrefixTest.php b/www/wiki/tests/phpunit/includes/search/SearchEnginePrefixTest.php new file mode 100644 index 00000000..3f59295a --- /dev/null +++ b/www/wiki/tests/phpunit/includes/search/SearchEnginePrefixTest.php @@ -0,0 +1,362 @@ +<?php + +use MediaWiki\MediaWikiServices; +use Wikimedia\TestingAccessWrapper; + +/** + * @group Search + * @group Database + */ +class SearchEnginePrefixTest extends MediaWikiLangTestCase { + private $originalHandlers; + + /** + * @var SearchEngine + */ + private $search; + + public function addDBDataOnce() { + if ( !$this->isWikitextNS( NS_MAIN ) ) { + // tests are skipped if NS_MAIN is not wikitext + return; + } + + $this->insertPage( 'Sandbox' ); + $this->insertPage( 'Bar' ); + $this->insertPage( 'Example' ); + $this->insertPage( 'Example Bar' ); + $this->insertPage( 'Example Foo' ); + $this->insertPage( 'Example Foo/Bar' ); + $this->insertPage( 'Example/Baz' ); + $this->insertPage( 'Sample' ); + $this->insertPage( 'Sample Ban' ); + $this->insertPage( 'Sample Eat' ); + $this->insertPage( 'Sample Who' ); + $this->insertPage( 'Sample Zoo' ); + $this->insertPage( 'Redirect test', '#REDIRECT [[Redirect Test]]' ); + $this->insertPage( 'Redirect Test' ); + $this->insertPage( 'Redirect Test Worse Result' ); + $this->insertPage( 'Redirect test2', '#REDIRECT [[Redirect Test2]]' ); + $this->insertPage( 'Redirect TEST2', '#REDIRECT [[Redirect Test2]]' ); + $this->insertPage( 'Redirect Test2' ); + $this->insertPage( 'Redirect Test2 Worse Result' ); + + $this->insertPage( 'Talk:Sandbox' ); + $this->insertPage( 'Talk:Example' ); + + $this->insertPage( 'User:Example' ); + } + + protected function setUp() { + parent::setUp(); + + if ( !$this->isWikitextNS( NS_MAIN ) ) { + $this->markTestSkipped( 'Main namespace does not support wikitext.' ); + } + + // Avoid special pages from extensions interferring with the tests + $this->setMwGlobals( [ + 'wgSpecialPages' => [], + 'wgHooks' => [], + ] ); + + $this->search = MediaWikiServices::getInstance()->newSearchEngine(); + $this->search->setNamespaces( [] ); + + $this->originalHandlers = TestingAccessWrapper::newFromClass( Hooks::class )->handlers; + TestingAccessWrapper::newFromClass( Hooks::class )->handlers = []; + + SpecialPageFactory::resetList(); + } + + public function tearDown() { + parent::tearDown(); + + TestingAccessWrapper::newFromClass( Hooks::class )->handlers = $this->originalHandlers; + + SpecialPageFactory::resetList(); + } + + protected function searchProvision( array $results = null ) { + if ( $results === null ) { + $this->setMwGlobals( 'wgHooks', [] ); + } else { + $this->setMwGlobals( 'wgHooks', [ + 'PrefixSearchBackend' => [ + function ( $namespaces, $search, $limit, &$srchres ) use ( $results ) { + $srchres = $results; + return false; + } + ], + ] ); + } + } + + public static function provideSearch() { + return [ + [ [ + 'Empty string', + 'query' => '', + 'results' => [], + ] ], + [ [ + 'Main namespace with title prefix', + 'query' => 'Sa', + 'results' => [ + 'Sample', + 'Sample Ban', + 'Sample Eat', + ], + // Third result when testing offset + 'offsetresult' => [ + 'Sample Who', + ], + ] ], + [ [ + 'Talk namespace prefix', + 'query' => 'Talk:', + 'results' => [ + 'Talk:Example', + 'Talk:Sandbox', + ], + ] ], + [ [ + 'User namespace prefix', + 'query' => 'User:', + 'results' => [ + 'User:Example', + ], + ] ], + [ [ + 'Special namespace prefix', + 'query' => 'Special:', + 'results' => [ + 'Special:ActiveUsers', + 'Special:AllMessages', + 'Special:AllMyUploads', + ], + // Third result when testing offset + 'offsetresult' => [ + 'Special:AllPages', + ], + ] ], + [ [ + 'Special namespace with prefix', + 'query' => 'Special:Un', + 'results' => [ + 'Special:Unblock', + 'Special:UncategorizedCategories', + 'Special:UncategorizedFiles', + ], + // Third result when testing offset + 'offsetresult' => [ + 'Special:UncategorizedPages', + ], + ] ], + [ [ + 'Special page name', + 'query' => 'Special:EditWatchlist', + 'results' => [ + 'Special:EditWatchlist', + ], + ] ], + [ [ + 'Special page subpages', + 'query' => 'Special:EditWatchlist/', + 'results' => [ + 'Special:EditWatchlist/clear', + 'Special:EditWatchlist/raw', + ], + ] ], + [ [ + 'Special page subpages with prefix', + 'query' => 'Special:EditWatchlist/cl', + 'results' => [ + 'Special:EditWatchlist/clear', + ], + ] ], + ]; + } + + /** + * @dataProvider provideSearch + * @covers SearchEngine::defaultPrefixSearch + */ + public function testSearch( array $case ) { + $this->search->setLimitOffset( 3 ); + $results = $this->search->defaultPrefixSearch( $case['query'] ); + $results = array_map( function ( Title $t ) { + return $t->getPrefixedText(); + }, $results ); + + $this->assertEquals( + $case['results'], + $results, + $case[0] + ); + } + + /** + * @dataProvider provideSearch + * @covers SearchEngine::defaultPrefixSearch + */ + public function testSearchWithOffset( array $case ) { + $this->search->setLimitOffset( 3, 1 ); + $results = $this->search->defaultPrefixSearch( $case['query'] ); + $results = array_map( function ( Title $t ) { + return $t->getPrefixedText(); + }, $results ); + + // We don't expect the first result when offsetting + array_shift( $case['results'] ); + // And sometimes we expect a different last result + $expected = isset( $case['offsetresult'] ) ? + array_merge( $case['results'], $case['offsetresult'] ) : + $case['results']; + + $this->assertEquals( + $expected, + $results, + $case[0] + ); + } + + public static function provideSearchBackend() { + return [ + [ [ + 'Simple case', + 'provision' => [ + 'Bar', + 'Barcelona', + 'Barbara', + ], + 'query' => 'Bar', + 'results' => [ + 'Bar', + 'Barcelona', + 'Barbara', + ], + ] ], + [ [ + 'Exact match not on top (T72958)', + 'provision' => [ + 'Barcelona', + 'Bar', + 'Barbara', + ], + 'query' => 'Bar', + 'results' => [ + 'Bar', + 'Barcelona', + 'Barbara', + ], + ] ], + [ [ + 'Exact match missing (T72958)', + 'provision' => [ + 'Barcelona', + 'Barbara', + 'Bart', + ], + 'query' => 'Bar', + 'results' => [ + 'Bar', + 'Barcelona', + 'Barbara', + ], + ] ], + [ [ + 'Exact match missing and not existing', + 'provision' => [ + 'Exile', + 'Exist', + 'External', + ], + 'query' => 'Ex', + 'results' => [ + 'Exile', + 'Exist', + 'External', + ], + ] ], + [ [ + "Exact match shouldn't override already found match if " . + "exact is redirect and found isn't", + 'provision' => [ + // Target of the exact match is low in the list + 'Redirect Test Worse Result', + 'Redirect Test', + ], + 'query' => 'redirect test', + 'results' => [ + // Redirect target is pulled up and exact match isn't added + 'Redirect Test', + 'Redirect Test Worse Result', + ], + ] ], + [ [ + "Exact match shouldn't override already found match if " . + "both exact match and found match are redirect", + 'provision' => [ + // Another redirect to the same target as the exact match + // is low in the list + 'Redirect Test2 Worse Result', + 'Redirect test2', + ], + 'query' => 'redirect TEST2', + 'results' => [ + // Found redirect is pulled to the top and exact match isn't + // added + 'Redirect test2', + 'Redirect Test2 Worse Result', + ], + ] ], + [ [ + "Exact match should override any already found matches that " . + "are redirects to it", + 'provision' => [ + // Another redirect to the same target as the exact match + // is low in the list + 'Redirect Test Worse Result', + 'Redirect test', + ], + 'query' => 'Redirect Test', + 'results' => [ + // Found redirect is pulled to the top and exact match isn't + // added + 'Redirect Test', + 'Redirect Test Worse Result', + 'Redirect test', + ], + ] ], + ]; + } + + /** + * @dataProvider provideSearchBackend + * @covers PrefixSearch::searchBackend + */ + public function testSearchBackend( array $case ) { + $search = $stub = $this->getMockBuilder( SearchEngine::class ) + ->setMethods( [ 'completionSearchBackend' ] )->getMock(); + + $return = SearchSuggestionSet::fromStrings( $case['provision'] ); + + $search->expects( $this->any() ) + ->method( 'completionSearchBackend' ) + ->will( $this->returnValue( $return ) ); + + $search->setLimitOffset( 3 ); + $results = $search->completionSearch( $case['query'] ); + + $results = $results->map( function ( SearchSuggestion $s ) { + return $s->getText(); + } ); + + $this->assertEquals( + $case['results'], + $results, + $case[0] + ); + } +} 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; + } +} diff --git a/www/wiki/tests/phpunit/includes/search/SearchIndexFieldTest.php b/www/wiki/tests/phpunit/includes/search/SearchIndexFieldTest.php new file mode 100644 index 00000000..8b4119e0 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/search/SearchIndexFieldTest.php @@ -0,0 +1,56 @@ +<?php + +/** + * @group Search + * @covers SearchIndexFieldDefinition + */ +class SearchIndexFieldTest extends MediaWikiTestCase { + + public function getMergeCases() { + return [ + [ 0, 'test', 0, 'test', true ], + [ SearchIndexField::INDEX_TYPE_NESTED, 'test', + SearchIndexField::INDEX_TYPE_NESTED, 'test', false ], + [ 0, 'test', 0, 'test2', true ], + [ 0, 'test', 1, 'test', false ], + ]; + } + + /** + * @dataProvider getMergeCases + * @param int $t1 + * @param string $n1 + * @param int $t2 + * @param string $n2 + * @param bool $result + */ + public function testMerge( $t1, $n1, $t2, $n2, $result ) { + $field1 = + $this->getMockBuilder( SearchIndexFieldDefinition::class ) + ->setMethods( [ 'getMapping' ] ) + ->setConstructorArgs( [ $n1, $t1 ] ) + ->getMock(); + $field2 = + $this->getMockBuilder( SearchIndexFieldDefinition::class ) + ->setMethods( [ 'getMapping' ] ) + ->setConstructorArgs( [ $n2, $t2 ] ) + ->getMock(); + + if ( $result ) { + $this->assertNotFalse( $field1->merge( $field2 ) ); + } else { + $this->assertFalse( $field1->merge( $field2 ) ); + } + + $field1->setFlag( 0xFF ); + $this->assertFalse( $field1->merge( $field2 ) ); + + $field1->setMergeCallback( + function ( $a, $b ) { + return "test"; + } + ); + $this->assertEquals( "test", $field1->merge( $field2 ) ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/search/SearchSuggestionSetTest.php b/www/wiki/tests/phpunit/includes/search/SearchSuggestionSetTest.php new file mode 100644 index 00000000..54533a73 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/search/SearchSuggestionSetTest.php @@ -0,0 +1,104 @@ +<?php + +/** + * Test for filter utilities. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +class SearchSuggestionSetTest extends \PHPUnit\Framework\TestCase { + /** + * Test that adding a new suggestion at the end + * will keep proper score ordering + */ + public function testAppend() { + $set = SearchSuggestionSet::emptySuggestionSet(); + $this->assertEquals( 0, $set->getSize() ); + $set->append( new SearchSuggestion( 3 ) ); + $this->assertEquals( 3, $set->getWorstScore() ); + $this->assertEquals( 3, $set->getBestScore() ); + + $suggestion = new SearchSuggestion( 4 ); + $set->append( $suggestion ); + $this->assertEquals( 2, $set->getWorstScore() ); + $this->assertEquals( 3, $set->getBestScore() ); + $this->assertEquals( 2, $suggestion->getScore() ); + + $suggestion = new SearchSuggestion( 2 ); + $set->append( $suggestion ); + $this->assertEquals( 1, $set->getWorstScore() ); + $this->assertEquals( 3, $set->getBestScore() ); + $this->assertEquals( 1, $suggestion->getScore() ); + + $scores = $set->map( function ( $s ) { + return $s->getScore(); + } ); + $sorted = $scores; + asort( $sorted ); + $this->assertEquals( $sorted, $scores ); + } + + /** + * Test that adding a new best suggestion will keep proper score + * ordering + */ + public function testInsertBest() { + $set = SearchSuggestionSet::emptySuggestionSet(); + $this->assertEquals( 0, $set->getSize() ); + $set->prepend( new SearchSuggestion( 3 ) ); + $this->assertEquals( 3, $set->getWorstScore() ); + $this->assertEquals( 3, $set->getBestScore() ); + + $suggestion = new SearchSuggestion( 4 ); + $set->prepend( $suggestion ); + $this->assertEquals( 3, $set->getWorstScore() ); + $this->assertEquals( 4, $set->getBestScore() ); + $this->assertEquals( 4, $suggestion->getScore() ); + + $suggestion = new SearchSuggestion( 0 ); + $set->prepend( $suggestion ); + $this->assertEquals( 3, $set->getWorstScore() ); + $this->assertEquals( 5, $set->getBestScore() ); + $this->assertEquals( 5, $suggestion->getScore() ); + + $suggestion = new SearchSuggestion( 2 ); + $set->prepend( $suggestion ); + $this->assertEquals( 3, $set->getWorstScore() ); + $this->assertEquals( 6, $set->getBestScore() ); + $this->assertEquals( 6, $suggestion->getScore() ); + + $scores = $set->map( function ( $s ) { + return $s->getScore(); + } ); + $sorted = $scores; + asort( $sorted ); + $this->assertEquals( $sorted, $scores ); + } + + public function testShrink() { + $set = SearchSuggestionSet::emptySuggestionSet(); + for ( $i = 0; $i < 100; $i++ ) { + $set->append( new SearchSuggestion( 0 ) ); + } + $set->shrink( 10 ); + $this->assertEquals( 10, $set->getSize() ); + + $set->shrink( 0 ); + $this->assertEquals( 0, $set->getSize() ); + } + + // TODO: test for fromTitles +} |