summaryrefslogtreecommitdiff
path: root/www/wiki/tests/phpunit/includes/search
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/tests/phpunit/includes/search')
-rw-r--r--www/wiki/tests/phpunit/includes/search/ParserOutputSearchDataExtractorTest.php70
-rw-r--r--www/wiki/tests/phpunit/includes/search/SearchEnginePrefixTest.php362
-rw-r--r--www/wiki/tests/phpunit/includes/search/SearchEngineTest.php368
-rw-r--r--www/wiki/tests/phpunit/includes/search/SearchIndexFieldTest.php56
-rw-r--r--www/wiki/tests/phpunit/includes/search/SearchSuggestionSetTest.php104
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
+}