PrefixSearchTest.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. <?php
  2. use Wikimedia\TestingAccessWrapper;
  3. /**
  4. * @group Search
  5. * @group Database
  6. * @covers PrefixSearch
  7. */
  8. class PrefixSearchTest extends MediaWikiLangTestCase {
  9. const NS_NONCAP = 12346;
  10. private $originalHandlers;
  11. public function addDBDataOnce() {
  12. if ( !$this->isWikitextNS( NS_MAIN ) ) {
  13. // tests are skipped if NS_MAIN is not wikitext
  14. return;
  15. }
  16. $this->insertPage( 'Sandbox' );
  17. $this->insertPage( 'Bar' );
  18. $this->insertPage( 'Example' );
  19. $this->insertPage( 'Example Bar' );
  20. $this->insertPage( 'Example Foo' );
  21. $this->insertPage( 'Example Foo/Bar' );
  22. $this->insertPage( 'Example/Baz' );
  23. $this->insertPage( 'Redirect test', '#REDIRECT [[Redirect Test]]' );
  24. $this->insertPage( 'Redirect Test' );
  25. $this->insertPage( 'Redirect Test Worse Result' );
  26. $this->insertPage( 'Redirect test2', '#REDIRECT [[Redirect Test2]]' );
  27. $this->insertPage( 'Redirect TEST2', '#REDIRECT [[Redirect Test2]]' );
  28. $this->insertPage( 'Redirect Test2' );
  29. $this->insertPage( 'Redirect Test2 Worse Result' );
  30. $this->insertPage( 'Talk:Sandbox' );
  31. $this->insertPage( 'Talk:Example' );
  32. $this->insertPage( 'User:Example' );
  33. $this->insertPage( Title::makeTitle( self::NS_NONCAP, 'Bar' ) );
  34. $this->insertPage( Title::makeTitle( self::NS_NONCAP, 'Upper' ) );
  35. $this->insertPage( Title::makeTitle( self::NS_NONCAP, 'sandbox' ) );
  36. }
  37. protected function setUp() {
  38. parent::setUp();
  39. if ( !$this->isWikitextNS( NS_MAIN ) ) {
  40. $this->markTestSkipped( 'Main namespace does not support wikitext.' );
  41. }
  42. // Avoid special pages from extensions interfering with the tests
  43. $this->setMwGlobals( [
  44. 'wgSpecialPages' => [],
  45. 'wgHooks' => [],
  46. 'wgExtraNamespaces' => [ self::NS_NONCAP => 'NonCap' ],
  47. 'wgCapitalLinkOverrides' => [ self::NS_NONCAP => false ],
  48. ] );
  49. $this->originalHandlers = TestingAccessWrapper::newFromClass( Hooks::class )->handlers;
  50. TestingAccessWrapper::newFromClass( Hooks::class )->handlers = [];
  51. $this->overrideMwServices();
  52. }
  53. public function tearDown() {
  54. parent::tearDown();
  55. TestingAccessWrapper::newFromClass( Hooks::class )->handlers = $this->originalHandlers;
  56. }
  57. protected function searchProvision( array $results = null ) {
  58. if ( $results === null ) {
  59. $this->setMwGlobals( 'wgHooks', [] );
  60. } else {
  61. $this->setMwGlobals( 'wgHooks', [
  62. 'PrefixSearchBackend' => [
  63. function ( $namespaces, $search, $limit, &$srchres ) use ( $results ) {
  64. $srchres = $results;
  65. return false;
  66. }
  67. ],
  68. ] );
  69. }
  70. }
  71. public static function provideSearch() {
  72. return [
  73. [ [
  74. 'Empty string',
  75. 'query' => '',
  76. 'results' => [],
  77. ] ],
  78. [ [
  79. 'Main namespace with title prefix',
  80. 'query' => 'Ex',
  81. 'results' => [
  82. 'Example',
  83. 'Example/Baz',
  84. 'Example Bar',
  85. ],
  86. // Third result when testing offset
  87. 'offsetresult' => [
  88. 'Example Foo',
  89. ],
  90. ] ],
  91. [ [
  92. 'Talk namespace prefix',
  93. 'query' => 'Talk:',
  94. 'results' => [
  95. 'Talk:Example',
  96. 'Talk:Sandbox',
  97. ],
  98. ] ],
  99. [ [
  100. 'User namespace prefix',
  101. 'query' => 'User:',
  102. 'results' => [
  103. 'User:Example',
  104. ],
  105. ] ],
  106. [ [
  107. 'Special namespace prefix',
  108. 'query' => 'Special:',
  109. 'results' => [
  110. 'Special:ActiveUsers',
  111. 'Special:AllMessages',
  112. 'Special:AllMyUploads',
  113. ],
  114. // Third result when testing offset
  115. 'offsetresult' => [
  116. 'Special:AllPages',
  117. ],
  118. ] ],
  119. [ [
  120. 'Special namespace with prefix',
  121. 'query' => 'Special:Un',
  122. 'results' => [
  123. 'Special:Unblock',
  124. 'Special:UncategorizedCategories',
  125. 'Special:UncategorizedFiles',
  126. ],
  127. // Third result when testing offset
  128. 'offsetresult' => [
  129. 'Special:UncategorizedPages',
  130. ],
  131. ] ],
  132. [ [
  133. 'Special page name',
  134. 'query' => 'Special:EditWatchlist',
  135. 'results' => [
  136. 'Special:EditWatchlist',
  137. ],
  138. ] ],
  139. [ [
  140. 'Special page subpages',
  141. 'query' => 'Special:EditWatchlist/',
  142. 'results' => [
  143. 'Special:EditWatchlist/clear',
  144. 'Special:EditWatchlist/raw',
  145. ],
  146. ] ],
  147. [ [
  148. 'Special page subpages with prefix',
  149. 'query' => 'Special:EditWatchlist/cl',
  150. 'results' => [
  151. 'Special:EditWatchlist/clear',
  152. ],
  153. ] ],
  154. [ [
  155. 'Namespace with case sensitive first letter',
  156. 'query' => 'NonCap:upper',
  157. 'results' => []
  158. ] ],
  159. [ [
  160. 'Multinamespace search',
  161. 'query' => 'B',
  162. 'results' => [
  163. 'Bar',
  164. 'NonCap:Bar',
  165. ],
  166. 'namespaces' => [ NS_MAIN, self::NS_NONCAP ],
  167. ] ],
  168. [ [
  169. 'Multinamespace search with lowercase first letter',
  170. 'query' => 'sand',
  171. 'results' => [
  172. 'Sandbox',
  173. 'NonCap:sandbox',
  174. ],
  175. 'namespaces' => [ NS_MAIN, self::NS_NONCAP ],
  176. ] ],
  177. ];
  178. }
  179. /**
  180. * @dataProvider provideSearch
  181. * @covers PrefixSearch::search
  182. * @covers PrefixSearch::searchBackend
  183. */
  184. public function testSearch( array $case ) {
  185. // FIXME: fails under postgres
  186. $this->markTestSkippedIfDbType( 'postgres' );
  187. $this->searchProvision( null );
  188. $namespaces = $case['namespaces'] ?? [];
  189. if ( wfGetDB( DB_REPLICA )->getType() === 'postgres' ) {
  190. // Postgres will sort lexicographically on utf8 code units (" " before "/")
  191. sort( $case['results'], SORT_STRING );
  192. }
  193. $searcher = new StringPrefixSearch;
  194. $results = $searcher->search( $case['query'], 3, $namespaces );
  195. $this->assertEquals(
  196. $case['results'],
  197. $results,
  198. $case[0]
  199. );
  200. }
  201. /**
  202. * @dataProvider provideSearch
  203. * @covers PrefixSearch::search
  204. * @covers PrefixSearch::searchBackend
  205. */
  206. public function testSearchWithOffset( array $case ) {
  207. // FIXME: fails under postgres
  208. $this->markTestSkippedIfDbType( 'postgres' );
  209. $this->searchProvision( null );
  210. $namespaces = $case['namespaces'] ?? [];
  211. $searcher = new StringPrefixSearch;
  212. $results = $searcher->search( $case['query'], 3, $namespaces, 1 );
  213. if ( wfGetDB( DB_REPLICA )->getType() === 'postgres' ) {
  214. // Postgres will sort lexicographically on utf8 code units (" " before "/")
  215. sort( $case['results'], SORT_STRING );
  216. }
  217. // We don't expect the first result when offsetting
  218. array_shift( $case['results'] );
  219. // And sometimes we expect a different last result
  220. $expected = isset( $case['offsetresult'] ) ?
  221. array_merge( $case['results'], $case['offsetresult'] ) :
  222. $case['results'];
  223. $this->assertEquals(
  224. $expected,
  225. $results,
  226. $case[0]
  227. );
  228. }
  229. public static function provideSearchBackend() {
  230. return [
  231. [ [
  232. 'Simple case',
  233. 'provision' => [
  234. 'Bar',
  235. 'Barcelona',
  236. 'Barbara',
  237. ],
  238. 'query' => 'Bar',
  239. 'results' => [
  240. 'Bar',
  241. 'Barcelona',
  242. 'Barbara',
  243. ],
  244. ] ],
  245. [ [
  246. 'Exact match not on top (T72958)',
  247. 'provision' => [
  248. 'Barcelona',
  249. 'Bar',
  250. 'Barbara',
  251. ],
  252. 'query' => 'Bar',
  253. 'results' => [
  254. 'Bar',
  255. 'Barcelona',
  256. 'Barbara',
  257. ],
  258. ] ],
  259. [ [
  260. 'Exact match missing (T72958)',
  261. 'provision' => [
  262. 'Barcelona',
  263. 'Barbara',
  264. 'Bart',
  265. ],
  266. 'query' => 'Bar',
  267. 'results' => [
  268. 'Bar',
  269. 'Barcelona',
  270. 'Barbara',
  271. ],
  272. ] ],
  273. [ [
  274. 'Exact match missing and not existing',
  275. 'provision' => [
  276. 'Exile',
  277. 'Exist',
  278. 'External',
  279. ],
  280. 'query' => 'Ex',
  281. 'results' => [
  282. 'Exile',
  283. 'Exist',
  284. 'External',
  285. ],
  286. ] ],
  287. [ [
  288. "Exact match shouldn't override already found match if " .
  289. "exact is redirect and found isn't",
  290. 'provision' => [
  291. // Target of the exact match is low in the list
  292. 'Redirect Test Worse Result',
  293. 'Redirect Test',
  294. ],
  295. 'query' => 'redirect test',
  296. 'results' => [
  297. // Redirect target is pulled up and exact match isn't added
  298. 'Redirect Test',
  299. 'Redirect Test Worse Result',
  300. ],
  301. ] ],
  302. [ [
  303. "Exact match shouldn't override already found match if " .
  304. "both exact match and found match are redirect",
  305. 'provision' => [
  306. // Another redirect to the same target as the exact match
  307. // is low in the list
  308. 'Redirect Test2 Worse Result',
  309. 'Redirect test2',
  310. ],
  311. 'query' => 'redirect TEST2',
  312. 'results' => [
  313. // Found redirect is pulled to the top and exact match isn't
  314. // added
  315. 'Redirect test2',
  316. 'Redirect Test2 Worse Result',
  317. ],
  318. ] ],
  319. [ [
  320. "Exact match should override any already found matches that " .
  321. "are redirects to it",
  322. 'provision' => [
  323. // Another redirect to the same target as the exact match
  324. // is low in the list
  325. 'Redirect Test Worse Result',
  326. 'Redirect test',
  327. ],
  328. 'query' => 'Redirect Test',
  329. 'results' => [
  330. // Found redirect is pulled to the top and exact match isn't
  331. // added
  332. 'Redirect Test',
  333. 'Redirect Test Worse Result',
  334. ],
  335. ] ],
  336. ];
  337. }
  338. /**
  339. * @dataProvider provideSearchBackend
  340. * @covers PrefixSearch::searchBackend
  341. */
  342. public function testSearchBackend( array $case ) {
  343. $this->searchProvision( $case['provision'] );
  344. $searcher = new StringPrefixSearch;
  345. $results = $searcher->search( $case['query'], 3 );
  346. $this->assertEquals(
  347. $case['results'],
  348. $results,
  349. $case[0]
  350. );
  351. }
  352. }