PageRenderTime 61ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 0ms

/modules/search/search.test

https://github.com/jianingy/guild-web
Unknown | 1337 lines | 1152 code | 185 blank | 0 comment | 0 complexity | 3ae6e3d37e0fb4b9c90460fa69e05a6b MD5 | raw file
  1. <?php
  2. // $Id: search.test,v 1.69 2010/08/10 01:11:36 dries Exp $
  3. // The search index can contain different types of content. Typically the type is 'node'.
  4. // Here we test with _test_ and _test2_ as the type.
  5. define('SEARCH_TYPE', '_test_');
  6. define('SEARCH_TYPE_2', '_test2_');
  7. define('SEARCH_TYPE_JPN', '_test3_');
  8. class SearchMatchTestCase extends DrupalWebTestCase {
  9. public static function getInfo() {
  10. return array(
  11. 'name' => 'Search engine queries',
  12. 'description' => 'Indexes content and queries it.',
  13. 'group' => 'Search',
  14. );
  15. }
  16. /**
  17. * Implementation setUp().
  18. */
  19. function setUp() {
  20. parent::setUp('search');
  21. }
  22. /**
  23. * Test search indexing.
  24. */
  25. function testMatching() {
  26. $this->_setup();
  27. $this->_testQueries();
  28. }
  29. /**
  30. * Set up a small index of items to test against.
  31. */
  32. function _setup() {
  33. variable_set('minimum_word_size', 3);
  34. for ($i = 1; $i <= 7; ++$i) {
  35. search_index($i, SEARCH_TYPE, $this->getText($i));
  36. }
  37. for ($i = 1; $i <= 5; ++$i) {
  38. search_index($i + 7, SEARCH_TYPE_2, $this->getText2($i));
  39. }
  40. // No getText builder function for Japanese text; just a simple array.
  41. foreach (array(
  42. 13 => '以呂波耳・ほへとち。リヌルヲ。',
  43. 14 => 'ドルーパルが大好きよ!',
  44. 15 => 'コーヒーとケーキ',
  45. ) as $i => $jpn) {
  46. search_index($i, SEARCH_TYPE_JPN, $jpn);
  47. }
  48. search_update_totals();
  49. }
  50. /**
  51. * _test_: Helper method for generating snippets of content.
  52. *
  53. * Generated items to test against:
  54. * 1 ipsum
  55. * 2 dolore sit
  56. * 3 sit am ut
  57. * 4 am ut enim am
  58. * 5 ut enim am minim veniam
  59. * 6 enim am minim veniam es cillum
  60. * 7 am minim veniam es cillum dolore eu
  61. */
  62. function getText($n) {
  63. $words = explode(' ', "Ipsum dolore sit am. Ut enim am minim veniam. Es cillum dolore eu.");
  64. return implode(' ', array_slice($words, $n - 1, $n));
  65. }
  66. /**
  67. * _test2_: Helper method for generating snippets of content.
  68. *
  69. * Generated items to test against:
  70. * 8 dear
  71. * 9 king philip
  72. * 10 philip came over
  73. * 11 came over from germany
  74. * 12 over from germany swimming
  75. */
  76. function getText2($n) {
  77. $words = explode(' ', "Dear King Philip came over from Germany swimming.");
  78. return implode(' ', array_slice($words, $n - 1, $n));
  79. }
  80. /**
  81. * Run predefine queries looking for indexed terms.
  82. */
  83. function _testQueries() {
  84. /*
  85. Note: OR queries that include short words in OR groups are only accepted
  86. if the ORed terms are ANDed with at least one long word in the rest of the query.
  87. e.g. enim dolore OR ut = enim (dolore OR ut) = (enim dolor) OR (enim ut) -> good
  88. e.g. dolore OR ut = (dolore) OR (ut) -> bad
  89. This is a design limitation to avoid full table scans.
  90. */
  91. $queries = array(
  92. // Simple AND queries.
  93. 'ipsum' => array(1),
  94. 'enim' => array(4, 5, 6),
  95. 'xxxxx' => array(),
  96. 'enim minim' => array(5, 6),
  97. 'enim xxxxx' => array(),
  98. 'dolore eu' => array(7),
  99. 'dolore xx' => array(),
  100. 'ut minim' => array(5),
  101. 'xx minim' => array(),
  102. 'enim veniam am minim ut' => array(5),
  103. // Simple OR queries.
  104. 'dolore OR ipsum' => array(1, 2, 7),
  105. 'dolore OR xxxxx' => array(2, 7),
  106. 'dolore OR ipsum OR enim' => array(1, 2, 4, 5, 6, 7),
  107. 'ipsum OR dolore sit OR cillum' => array(2, 7),
  108. 'minim dolore OR ipsum' => array(7),
  109. 'dolore OR ipsum veniam' => array(7),
  110. 'minim dolore OR ipsum OR enim' => array(5, 6, 7),
  111. 'dolore xx OR yy' => array(),
  112. 'xxxxx dolore OR ipsum' => array(),
  113. // Negative queries.
  114. 'dolore -sit' => array(7),
  115. 'dolore -eu' => array(2),
  116. 'dolore -xxxxx' => array(2, 7),
  117. 'dolore -xx' => array(2, 7),
  118. // Phrase queries.
  119. '"dolore sit"' => array(2),
  120. '"sit dolore"' => array(),
  121. '"am minim veniam es"' => array(6, 7),
  122. '"minim am veniam es"' => array(),
  123. // Mixed queries.
  124. '"am minim veniam es" OR dolore' => array(2, 6, 7),
  125. '"minim am veniam es" OR "dolore sit"' => array(2),
  126. '"minim am veniam es" OR "sit dolore"' => array(),
  127. '"am minim veniam es" -eu' => array(6),
  128. '"am minim veniam" -"cillum dolore"' => array(5, 6),
  129. '"am minim veniam" -"dolore cillum"' => array(5, 6, 7),
  130. 'xxxxx "minim am veniam es" OR dolore' => array(),
  131. 'xx "minim am veniam es" OR dolore' => array()
  132. );
  133. foreach ($queries as $query => $results) {
  134. $result = db_select('search_index', 'i')
  135. ->extend('SearchQuery')
  136. ->searchExpression($query, SEARCH_TYPE)
  137. ->execute();
  138. $set = $result ? $result->fetchAll() : array();
  139. $this->_testQueryMatching($query, $set, $results);
  140. $this->_testQueryScores($query, $set, $results);
  141. }
  142. // These queries are run against the second index type, SEARCH_TYPE_2.
  143. $queries = array(
  144. // Simple AND queries.
  145. 'ipsum' => array(),
  146. 'enim' => array(),
  147. 'enim minim' => array(),
  148. 'dear' => array(8),
  149. 'germany' => array(11, 12),
  150. );
  151. foreach ($queries as $query => $results) {
  152. $result = db_select('search_index', 'i')
  153. ->extend('SearchQuery')
  154. ->searchExpression($query, SEARCH_TYPE_2)
  155. ->execute();
  156. $set = $result ? $result->fetchAll() : array();
  157. $this->_testQueryMatching($query, $set, $results);
  158. $this->_testQueryScores($query, $set, $results);
  159. }
  160. // These queries are run against the third index type, SEARCH_TYPE_JPN.
  161. $queries = array(
  162. // Simple AND queries.
  163. '呂波耳' => array(13),
  164. '以呂波耳' => array(13),
  165. 'ほへと ヌルヲ' => array(13),
  166. 'とちリ' => array(),
  167. 'ドルーパル' => array(14),
  168. 'パルが大' => array(14),
  169. 'コーヒー' => array(15),
  170. 'ヒーキ' => array(),
  171. );
  172. foreach ($queries as $query => $results) {
  173. $result = db_select('search_index', 'i')
  174. ->extend('SearchQuery')
  175. ->searchExpression($query, SEARCH_TYPE_JPN)
  176. ->execute();
  177. $set = $result ? $result->fetchAll() : array();
  178. $this->_testQueryMatching($query, $set, $results);
  179. $this->_testQueryScores($query, $set, $results);
  180. }
  181. }
  182. /**
  183. * Test the matching abilities of the engine.
  184. *
  185. * Verify if a query produces the correct results.
  186. */
  187. function _testQueryMatching($query, $set, $results) {
  188. // Get result IDs.
  189. $found = array();
  190. foreach ($set as $item) {
  191. $found[] = $item->sid;
  192. }
  193. // Compare $results and $found.
  194. sort($found);
  195. sort($results);
  196. $this->assertEqual($found, $results, "Query matching '$query'");
  197. }
  198. /**
  199. * Test the scoring abilities of the engine.
  200. *
  201. * Verify if a query produces normalized, monotonous scores.
  202. */
  203. function _testQueryScores($query, $set, $results) {
  204. // Get result scores.
  205. $scores = array();
  206. foreach ($set as $item) {
  207. $scores[] = $item->calculated_score;
  208. }
  209. // Check order.
  210. $sorted = $scores;
  211. sort($sorted);
  212. $this->assertEqual($scores, array_reverse($sorted), "Query order '$query'");
  213. // Check range.
  214. $this->assertEqual(!count($scores) || (min($scores) > 0.0 && max($scores) <= 1.0001), TRUE, "Query scoring '$query'");
  215. }
  216. }
  217. class SearchBikeShed extends DrupalWebTestCase {
  218. protected $searching_user;
  219. public static function getInfo() {
  220. return array(
  221. 'name' => 'Bike shed',
  222. 'description' => 'Tests the bike shed text on the no results page.',
  223. 'group' => 'Search'
  224. );
  225. }
  226. function setUp() {
  227. parent::setUp('search');
  228. // Create user.
  229. $this->searching_user = $this->drupalCreateUser(array('search content'));
  230. }
  231. function testFailedSearch() {
  232. $this->drupalLogin($this->searching_user);
  233. $this->drupalGet('search/node');
  234. $this->assertText(t('Enter your keywords'));
  235. $edit = array();
  236. $edit['keys'] = 'bike shed ' . $this->randomName();
  237. $this->drupalPost('search/node', $edit, t('Search'));
  238. $this->assertText(t('Consider loosening your query with OR. bike OR shed will often show more results than bike shed.'), t('Help text is displayed when search returns no results.'));
  239. }
  240. }
  241. class SearchAdvancedSearchForm extends DrupalWebTestCase {
  242. protected $node;
  243. public static function getInfo() {
  244. return array(
  245. 'name' => 'Advanced search form',
  246. 'description' => 'Indexes content and tests the advanced search form.',
  247. 'group' => 'Search',
  248. );
  249. }
  250. function setUp() {
  251. parent::setUp('search');
  252. // Create and login user.
  253. $test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'administer nodes'));
  254. $this->drupalLogin($test_user);
  255. // Create initial node.
  256. $node = $this->drupalCreateNode();
  257. $this->node = $this->drupalCreateNode();
  258. // First update the index. This does the initial processing.
  259. node_update_index();
  260. // Then, run the shutdown function. Testing is a unique case where indexing
  261. // and searching has to happen in the same request, so running the shutdown
  262. // function manually is needed to finish the indexing process.
  263. search_update_totals();
  264. }
  265. /**
  266. * Test using the search form with GET and POST queries.
  267. * Test using the advanced search form to limit search to nodes of type "Basic page".
  268. */
  269. function testNodeType() {
  270. $this->assertTrue($this->node->type == 'page', t('Node type is Basic page.'));
  271. // Assert that the dummy title doesn't equal the real title.
  272. $dummy_title = 'Lorem ipsum';
  273. $this->assertNotEqual($dummy_title, $this->node->title, t("Dummy title doens't equal node title"));
  274. // Search for the dummy title with a GET query.
  275. $this->drupalGet('search/node/' . $dummy_title);
  276. $this->assertNoText($this->node->title, t('Basic page node is not found with dummy title.'));
  277. // Search for the title of the node with a GET query.
  278. $this->drupalGet('search/node/' . $this->node->title);
  279. $this->assertText($this->node->title, t('Basic page node is found with GET query.'));
  280. // Search for the title of the node with a POST query.
  281. $edit = array('or' => $this->node->title);
  282. $this->drupalPost('search/node', $edit, t('Advanced search'));
  283. $this->assertText($this->node->title, t('Basic page node is found with POST query.'));
  284. // Advanced search type option.
  285. $this->drupalPost('search/node', array_merge($edit, array('type[page]' => 'page')), t('Advanced search'));
  286. $this->assertText($this->node->title, t('Basic page node is found with POST query and type:page.'));
  287. $this->drupalPost('search/node', array_merge($edit, array('type[article]' => 'article')), t('Advanced search'));
  288. $this->assertText('bike shed', t('Article node is not found with POST query and type:article.'));
  289. }
  290. }
  291. class SearchRankingTestCase extends DrupalWebTestCase {
  292. public static function getInfo() {
  293. return array(
  294. 'name' => 'Search engine ranking',
  295. 'description' => 'Indexes content and tests ranking factors.',
  296. 'group' => 'Search',
  297. );
  298. }
  299. /**
  300. * Implementation setUp().
  301. */
  302. function setUp() {
  303. parent::setUp('search', 'statistics', 'comment');
  304. }
  305. function testRankings() {
  306. // Login with sufficient privileges.
  307. $this->drupalLogin($this->drupalCreateUser(array('post comments without approval', 'create page content')));
  308. // Build a list of the rankings to test.
  309. $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views');
  310. // Create nodes for testing.
  311. foreach ($node_ranks as $node_rank) {
  312. $settings = array('type' => 'page', 'title' => array(LANGUAGE_NONE => array(array('value' => 'Drupal rocks'))), 'body' => array(LANGUAGE_NONE => array(array('value' => "Drupal's search rocks"))));
  313. foreach (array(0, 1) as $num) {
  314. if ($num == 1) {
  315. switch ($node_rank) {
  316. case 'sticky':
  317. case 'promote':
  318. $settings[$node_rank] = 1;
  319. break;
  320. case 'relevance':
  321. $settings['body'][LANGUAGE_NONE][0]['value'] .= " really rocks";
  322. break;
  323. case 'recent':
  324. $settings['created'] = REQUEST_TIME + 3600;
  325. break;
  326. case 'comments':
  327. $settings['comment'] = 2;
  328. break;
  329. }
  330. }
  331. $nodes[$node_rank][$num] = $this->drupalCreateNode($settings);
  332. }
  333. }
  334. // Update the search index.
  335. module_invoke_all('update_index');
  336. search_update_totals();
  337. // Refresh variables after the treatment.
  338. $this->refreshVariables();
  339. // Add a comment to one of the nodes.
  340. $edit = array();
  341. $edit['subject'] = 'my comment title';
  342. $edit['comment_body[' . LANGUAGE_NONE . '][0][value]'] = 'some random comment';
  343. $this->drupalGet('comment/reply/' . $nodes['comments'][1]->nid);
  344. $this->drupalPost(NULL, $edit, t('Preview'));
  345. $this->drupalPost(NULL, $edit, t('Save'));
  346. // Enable counting of statistics.
  347. variable_set('statistics_count_content_views', 1);
  348. // Then View one of the nodes a bunch of times.
  349. for ($i = 0; $i < 5; $i ++) {
  350. $this->drupalGet('node/' . $nodes['views'][1]->nid);
  351. }
  352. // Test each of the possible rankings.
  353. foreach ($node_ranks as $node_rank) {
  354. // Disable all relevancy rankings except the one we are testing.
  355. foreach ($node_ranks as $var) {
  356. variable_set('node_rank_' . $var, $var == $node_rank ? 10 : 0);
  357. }
  358. // Do the search and assert the results.
  359. $set = node_search_execute('rocks');
  360. $this->assertEqual($set[0]['node']->nid, $nodes[$node_rank][1]->nid, 'Search ranking "' . $node_rank . '" order.');
  361. }
  362. }
  363. /**
  364. * Test rankings of HTML tags.
  365. */
  366. function testHTMLRankings() {
  367. // Login with sufficient privileges.
  368. $this->drupalLogin($this->drupalCreateUser(array('create page content')));
  369. // Test HTML tags with different weights.
  370. $sorted_tags = array('h1', 'h2', 'h3', 'h4', 'a', 'h5', 'h6', 'notag');
  371. $shuffled_tags = $sorted_tags;
  372. // Shuffle tags to ensure HTML tags are ranked properly.
  373. shuffle($shuffled_tags);
  374. $settings = array(
  375. 'type' => 'page',
  376. 'title' => array(LANGUAGE_NONE => array(array('value' => 'Simple node'))),
  377. );
  378. foreach ($shuffled_tags as $tag) {
  379. switch ($tag) {
  380. case 'a':
  381. $settings['body'] = array(LANGUAGE_NONE => array(array('value' => l('Drupal Rocks', 'node'), 'format' => 3)));
  382. break;
  383. case 'notag':
  384. $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'Drupal Rocks')));
  385. break;
  386. default:
  387. $settings['body'] = array(LANGUAGE_NONE => array(array('value' => "<$tag>Drupal Rocks</$tag>", 'format' => 3)));
  388. break;
  389. }
  390. $nodes[$tag] = $this->drupalCreateNode($settings);
  391. }
  392. // Update the search index.
  393. module_invoke_all('update_index');
  394. search_update_totals();
  395. // Refresh variables after the treatment.
  396. $this->refreshVariables();
  397. // Disable all other rankings.
  398. $node_ranks = array('sticky', 'promote', 'recent', 'comments', 'views');
  399. foreach ($node_ranks as $node_rank) {
  400. variable_set('node_rank_' . $node_rank, 0);
  401. }
  402. $set = node_search_execute('rocks');
  403. // Test the ranking of each tag.
  404. foreach ($sorted_tags as $tag_rank => $tag) {
  405. // Assert the results.
  406. if ($tag == 'notag') {
  407. $this->assertEqual($set[$tag_rank]['node']->nid, $nodes[$tag]->nid, 'Search tag ranking for plain text order.');
  408. } else {
  409. $this->assertEqual($set[$tag_rank]['node']->nid, $nodes[$tag]->nid, 'Search tag ranking for "&lt;' . $sorted_tags[$tag_rank] . '&gt;" order.');
  410. }
  411. }
  412. // Test tags with the same weight against the sorted tags.
  413. $unsorted_tags = array('u', 'b', 'i', 'strong', 'em');
  414. foreach ($unsorted_tags as $tag) {
  415. $settings['body'] = array(LANGUAGE_NONE => array(array('value' => "<$tag>Drupal Rocks</$tag>", 'format' => 3)));
  416. $node = $this->drupalCreateNode($settings);
  417. // Update the search index.
  418. module_invoke_all('update_index');
  419. search_update_totals();
  420. // Refresh variables after the treatment.
  421. $this->refreshVariables();
  422. $set = node_search_execute('rocks');
  423. // Ranking should always be second to last.
  424. $set = array_slice($set, -2, 1);
  425. // Assert the results.
  426. $this->assertEqual($set[0]['node']->nid, $node->nid, 'Search tag ranking for "&lt;' . $tag . '&gt;" order.');
  427. // Delete node so it doesn't show up in subsequent search results.
  428. node_delete($node->nid);
  429. }
  430. }
  431. /**
  432. * Verifies that if we combine two rankings, search still works.
  433. *
  434. * See issue http://drupal.org/node/771596
  435. */
  436. function testDoubleRankings() {
  437. // Login with sufficient privileges.
  438. $this->drupalLogin($this->drupalCreateUser(array('post comments without approval', 'create page content')));
  439. // See testRankings() above - build a node that will rank high for sticky.
  440. $settings = array(
  441. 'type' => 'page',
  442. 'title' => array(LANGUAGE_NONE => array(array('value' => 'Drupal rocks'))),
  443. 'body' => array(LANGUAGE_NONE => array(array('value' => "Drupal's search rocks"))),
  444. 'sticky' => 1,
  445. );
  446. $node = $this->drupalCreateNode($settings);
  447. // Update the search index.
  448. module_invoke_all('update_index');
  449. search_update_totals();
  450. // Refresh variables after the treatment.
  451. $this->refreshVariables();
  452. // Set up for ranking sticky and lots of comments; make sure others are
  453. // disabled.
  454. $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views');
  455. foreach ($node_ranks as $var) {
  456. $value = ($var == 'sticky' || $var == 'comments') ? 10 : 0;
  457. variable_set('node_rank_' . $var, $value);
  458. }
  459. // Do the search and assert the results.
  460. $set = node_search_execute('rocks');
  461. $this->assertEqual($set[0]['node']->nid, $node->nid, 'Search double ranking order.');
  462. }
  463. }
  464. class SearchBlockTestCase extends DrupalWebTestCase {
  465. public static function getInfo() {
  466. return array(
  467. 'name' => 'Block availability',
  468. 'description' => 'Check if the search form block is available.',
  469. 'group' => 'Search',
  470. );
  471. }
  472. function setUp() {
  473. parent::setUp('search');
  474. // Create and login user
  475. $admin_user = $this->drupalCreateUser(array('administer blocks', 'search content'));
  476. $this->drupalLogin($admin_user);
  477. }
  478. function testSearchFormBlock() {
  479. // Set block title to confirm that the interface is availble.
  480. $this->drupalPost('admin/structure/block/manage/search/form/configure', array('title' => $this->randomName(8)), t('Save block'));
  481. $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.'));
  482. // Set the block to a region to confirm block is availble.
  483. $edit = array();
  484. $edit['search_form[region]'] = 'footer';
  485. $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
  486. $this->assertText(t('The block settings have been updated.'), t('Block successfully move to footer region.'));
  487. }
  488. /**
  489. * Test that the search block form works correctly.
  490. */
  491. function testBlock() {
  492. // Enable the block, and place it in the 'content' region so that it isn't
  493. // hidden on 404 pages.
  494. $edit = array('search_form[region]' => 'content');
  495. $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
  496. // Test a normal search via the block form, from the front page.
  497. $terms = array('search_block_form' => 'test');
  498. $this->drupalPost('node', $terms, t('Search'));
  499. $this->assertText('Your search yielded no results');
  500. // Test a search from the block on a 404 page.
  501. $this->drupalGet('foo');
  502. $this->assertResponse(404);
  503. $this->drupalPost(NULL, $terms, t('Search'));
  504. $this->assertResponse(200);
  505. $this->assertText('Your search yielded no results');
  506. // Test a search from the block when it doesn't appear on the search page.
  507. $edit = array('pages' => 'search');
  508. $this->drupalPost('admin/structure/block/manage/search/form/configure', $edit, t('Save block'));
  509. $this->drupalPost('node', $terms, t('Search'));
  510. $this->assertText('Your search yielded no results');
  511. }
  512. }
  513. /**
  514. * Tests that searching for a phrase gets the correct page count.
  515. */
  516. class SearchExactTestCase extends DrupalWebTestCase {
  517. public static function getInfo() {
  518. return array(
  519. 'name' => 'Search engine phrase queries',
  520. 'description' => 'Tests that searching for a phrase gets the correct page count.',
  521. 'group' => 'Search',
  522. );
  523. }
  524. function setUp() {
  525. parent::setUp('search');
  526. }
  527. /**
  528. * Tests that the correct number of pager links are found for both keywords and phrases.
  529. */
  530. function testExactQuery() {
  531. // Login with sufficient privileges.
  532. $this->drupalLogin($this->drupalCreateUser(array('create page content', 'search content')));
  533. $settings = array(
  534. 'type' => 'page',
  535. 'title' => 'Simple Node',
  536. );
  537. // Create nodes with exact phrase.
  538. for ($i = 0; $i <= 17; $i++) {
  539. $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love pizza')));
  540. $this->drupalCreateNode($settings);
  541. }
  542. // Create nodes containing keywords.
  543. for ($i = 0; $i <= 17; $i++) {
  544. $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love cheesy pizza')));
  545. $this->drupalCreateNode($settings);
  546. }
  547. // Update the search index.
  548. module_invoke_all('update_index');
  549. search_update_totals();
  550. // Refresh variables after the treatment.
  551. $this->refreshVariables();
  552. // Test that the correct number of pager links are found for keyword search.
  553. $edit = array('keys' => 'love pizza');
  554. $this->drupalPost('search/node', $edit, t('Search'));
  555. $this->assertLinkByHref('page=1', 0, '2nd page link is found for keyword search.');
  556. $this->assertLinkByHref('page=2', 0, '3rd page link is found for keyword search.');
  557. $this->assertLinkByHref('page=3', 0, '4th page link is found for keyword search.');
  558. $this->assertNoLinkByHref('page=4', '5th page link is not found for keyword search.');
  559. // Test that the correct number of pager links are found for exact phrase search.
  560. $edit = array('keys' => '"love pizza"');
  561. $this->drupalPost('search/node', $edit, t('Search'));
  562. $this->assertLinkByHref('page=1', 0, '2nd page link is found for exact phrase search.');
  563. $this->assertNoLinkByHref('page=2', '3rd page link is not found for exact phrase search.');
  564. }
  565. }
  566. /**
  567. * Test integration searching comments.
  568. */
  569. class SearchCommentTestCase extends DrupalWebTestCase {
  570. protected $admin_user;
  571. public static function getInfo() {
  572. return array(
  573. 'name' => 'Comment Search tests',
  574. 'description' => 'Verify text formats and filters used elsewhere.',
  575. 'group' => 'Search',
  576. );
  577. }
  578. function setUp() {
  579. parent::setUp('comment', 'search');
  580. // Create and log in an administrative user having access to the Full HTML
  581. // text format.
  582. $full_html_format = db_query_range('SELECT * FROM {filter_format} WHERE name = :name', 0, 1, array(':name' => 'Full HTML'))->fetchObject();
  583. $permissions = array(
  584. 'administer filters',
  585. filter_permission_name($full_html_format),
  586. 'administer permissions',
  587. 'create page content',
  588. 'post comments without approval',
  589. 'access comments',
  590. );
  591. $this->admin_user = $this->drupalCreateUser($permissions);
  592. $this->drupalLogin($this->admin_user);
  593. }
  594. /**
  595. * Verify that comments are rendered using proper format in search results.
  596. */
  597. function testSearchResultsComment() {
  598. $comment_body = 'Test comment body';
  599. variable_set('comment_preview_article', DRUPAL_OPTIONAL);
  600. // Enable check_plain() for 'Filtered HTML' text format.
  601. $filtered_html_format_id = db_query_range('SELECT format FROM {filter_format} WHERE name = :name', 0, 1, array(':name' => 'Filtered HTML'))->fetchField();
  602. $edit = array(
  603. 'filters[filter_html_escape][status]' => $filtered_html_format_id,
  604. );
  605. $this->drupalPost('admin/config/content/formats/1', $edit, t('Save configuration'));
  606. // Allow anonymous users to search content.
  607. $edit = array(
  608. DRUPAL_ANONYMOUS_RID . '[search content]' => 1,
  609. DRUPAL_ANONYMOUS_RID . '[access comments]' => 1,
  610. DRUPAL_ANONYMOUS_RID . '[post comments]' => 1,
  611. );
  612. $this->drupalPost('admin/people/permissions', $edit, t('Save permissions'));
  613. // Create a node.
  614. $node = $this->drupalCreateNode(array('type' => 'article'));
  615. // Post a comment using 'Full HTML' text format.
  616. $edit_comment = array();
  617. $edit_comment['subject'] = 'Test comment subject';
  618. $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '<h1>' . $comment_body . '</h1>';
  619. $full_html_format_id = db_query_range('SELECT format FROM {filter_format} WHERE name = :name', 0, 1, array(':name' => 'Full HTML'))->fetchField();
  620. $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $full_html_format_id;
  621. $this->drupalPost('comment/reply/' . $node->nid, $edit_comment, t('Save'));
  622. // Invoke search index update.
  623. $this->drupalLogout();
  624. $this->cronRun();
  625. // Search for the comment subject.
  626. $edit = array(
  627. 'search_block_form' => "'" . $edit_comment['subject'] . "'",
  628. );
  629. $this->drupalPost('', $edit, t('Search'));
  630. $this->assertText($node->title, t('Node found in search results.'));
  631. $this->assertText($edit_comment['subject'], t('Comment subject found in search results.'));
  632. // Search for the comment body.
  633. $edit = array(
  634. 'search_block_form' => "'" . $comment_body . "'",
  635. );
  636. $this->drupalPost('', $edit, t('Search'));
  637. $this->assertText($node->title, t('Node found in search results.'));
  638. // Verify that comment is rendered using proper format.
  639. $this->assertText($comment_body, t('Comment body text found in search results.'));
  640. $this->assertNoRaw(t('n/a'), t('HTML in comment body is not hidden.'));
  641. $this->assertNoRaw(check_plain($edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]']), t('HTML in comment body is not escaped.'));
  642. // Hide comments.
  643. $this->drupalLogin($this->admin_user);
  644. $node->comment = 0;
  645. node_save($node);
  646. // Invoke search index update.
  647. $this->drupalLogout();
  648. $this->cronRun();
  649. // Search for $title.
  650. $this->drupalPost('', $edit, t('Search'));
  651. $this->assertNoText($comment_body, t('Comment body text not found in search results.'));
  652. }
  653. /**
  654. * Verify access rules for comment indexing with different permissions.
  655. */
  656. function testSearchResultsCommentAccess() {
  657. $comment_body = 'Test comment body';
  658. $this->comment_subject = 'Test comment subject';
  659. $this->admin_role = $this->admin_user->roles;
  660. unset($this->admin_role[DRUPAL_AUTHENTICATED_RID]);
  661. $this->admin_role = key($this->admin_role);
  662. // Create a node.
  663. variable_set('comment_preview_article', DRUPAL_OPTIONAL);
  664. $this->node = $this->drupalCreateNode(array('type' => 'article'));
  665. // Post a comment using 'Full HTML' text format.
  666. $edit_comment = array();
  667. $edit_comment['subject'] = $this->comment_subject;
  668. $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '<h1>' . $comment_body . '</h1>';
  669. $this->drupalPost('comment/reply/' . $this->node->nid, $edit_comment, t('Save'));
  670. $this->drupalLogout();
  671. $this->setRolePermissions(DRUPAL_ANONYMOUS_RID);
  672. $this->checkCommentAccess('Anon user has search permission but no access comments permission, comments should not be indexed');
  673. $this->setRolePermissions(DRUPAL_ANONYMOUS_RID, TRUE);
  674. $this->checkCommentAccess('Anon user has search permission and access comments permission, comments should be indexed', TRUE);
  675. $this->drupalLogin($this->admin_user);
  676. $this->drupalGet('admin/people/permissions');
  677. // Disable search access for authenticated user to test admin user.
  678. $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, FALSE, FALSE);
  679. $this->setRolePermissions($this->admin_role);
  680. $this->checkCommentAccess('Admin user has search permission but no access comments permission, comments should not be indexed');
  681. $this->setRolePermissions($this->admin_role, TRUE);
  682. $this->checkCommentAccess('Admin user has search permission and access comments permission, comments should be indexed', TRUE);
  683. $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID);
  684. $this->checkCommentAccess('Authenticated user has search permission but no access comments permission, comments should not be indexed');
  685. $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE);
  686. $this->checkCommentAccess('Authenticated user has search permission and access comments permission, comments should be indexed', TRUE);
  687. // Verify that access comments permission is inherited from the
  688. // authenticated role.
  689. $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, FALSE);
  690. $this->setRolePermissions($this->admin_role);
  691. $this->checkCommentAccess('Admin user has search permission and no access comments permission, but comments should be indexed because admin user inherits authenticated user\'s permission to access comments', TRUE);
  692. // Verify that search content permission is inherited from the authenticated
  693. // role.
  694. $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, TRUE);
  695. $this->setRolePermissions($this->admin_role, TRUE, FALSE);
  696. $this->checkCommentAccess('Admin user has access comments permission and no search permission, but comments should be indexed because admin user inherits authenticated user\'s permission to search', TRUE);
  697. }
  698. /**
  699. * Set permissions for role.
  700. */
  701. function setRolePermissions($rid, $access_comments = FALSE, $search_content = TRUE) {
  702. $permissions = array(
  703. 'access comments' => $access_comments,
  704. 'search content' => $search_content,
  705. );
  706. user_role_change_permissions($rid, $permissions);
  707. }
  708. /**
  709. * Update search index and search for comment.
  710. */
  711. function checkCommentAccess($message, $assume_access = FALSE) {
  712. // Invoke search index update.
  713. search_touch_node($this->node->nid);
  714. $this->cronRun();
  715. // Search for the comment subject.
  716. $edit = array(
  717. 'search_block_form' => "'" . $this->comment_subject . "'",
  718. );
  719. $this->drupalPost('', $edit, t('Search'));
  720. $method = $assume_access ? 'assertText' : 'assertNoText';
  721. $verb = $assume_access ? 'found' : 'not found';
  722. $this->{$method}($this->node->title, "Node $verb in search results: " . $message);
  723. $this->{$method}($this->comment_subject, "Comment subject $verb in search results: " . $message);
  724. }
  725. }
  726. /**
  727. * Tests that comment count display toggles properly on comment status of node
  728. *
  729. * Issue 537278
  730. *
  731. * - Nodes with comment status set to Open should always how comment counts
  732. * - Nodes with comment status set to Closed should show comment counts
  733. * only when there are comments
  734. * - Nodes with comment status set to Hidden should never show comment counts
  735. */
  736. class SearchCommentCountToggleTestCase extends DrupalWebTestCase {
  737. protected $searching_user;
  738. protected $searchable_nodes;
  739. public static function getInfo() {
  740. return array(
  741. 'name' => 'Comment count toggle',
  742. 'description' => 'Verify that comment count display toggles properly on comment status of node.',
  743. 'group' => 'Search',
  744. );
  745. }
  746. function setUp() {
  747. parent::setUp('search');
  748. // Create searching user.
  749. $this->searching_user = $this->drupalCreateUser(array('search content', 'access content', 'access comments', 'post comments without approval'));
  750. // Create initial nodes.
  751. $node_params = array('type' => 'article', 'body' => array(LANGUAGE_NONE => array(array('value' => 'SearchCommentToggleTestCase'))));
  752. $this->searchable_nodes['1 comment'] = $this->drupalCreateNode($node_params);
  753. $this->searchable_nodes['0 comments'] = $this->drupalCreateNode($node_params);
  754. // Login with sufficient privileges.
  755. $this->drupalLogin($this->searching_user);
  756. // Create a comment array
  757. $edit_comment = array();
  758. $edit_comment['subject'] = $this->randomName();
  759. $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = $this->randomName();
  760. $filtered_html_format_id = db_query_range('SELECT format FROM {filter_format} WHERE name = :name', 0, 1, array(':name' => 'Filtered HTML'))->fetchField();
  761. $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $filtered_html_format_id;
  762. // Post comment to the test node with comment
  763. $this->drupalPost('comment/reply/' . $this->searchable_nodes['1 comment']->nid, $edit_comment, t('Save'));
  764. // First update the index. This does the initial processing.
  765. node_update_index();
  766. // Then, run the shutdown function. Testing is a unique case where indexing
  767. // and searching has to happen in the same request, so running the shutdown
  768. // function manually is needed to finish the indexing process.
  769. search_update_totals();
  770. }
  771. /**
  772. * Verify that comment count display toggles properly on comment status of node
  773. */
  774. function testSearchCommentCountToggle() {
  775. // Search for the nodes by string in the node body.
  776. $edit = array(
  777. 'search_block_form' => "'SearchCommentToggleTestCase'",
  778. );
  779. // Test comment count display for nodes with comment status set to Open
  780. $this->drupalPost('', $edit, t('Search'));
  781. $this->assertText(t('0 comments'), t('Empty comment count displays for nodes with comment status set to Open'));
  782. $this->assertText(t('1 comment'), t('Non-empty comment count displays for nodes with comment status set to Open'));
  783. // Test comment count display for nodes with comment status set to Closed
  784. $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_CLOSED;
  785. node_save($this->searchable_nodes['0 comments']);
  786. $this->searchable_nodes['1 comment']->comment = COMMENT_NODE_CLOSED;
  787. node_save($this->searchable_nodes['1 comment']);
  788. $this->drupalPost('', $edit, t('Search'));
  789. $this->assertNoText(t('0 comments'), t('Empty comment count does not display for nodes with comment status set to Closed'));
  790. $this->assertText(t('1 comment'), t('Non-empty comment count displays for nodes with comment status set to Closed'));
  791. // Test comment count display for nodes with comment status set to Hidden
  792. $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_HIDDEN;
  793. node_save($this->searchable_nodes['0 comments']);
  794. $this->searchable_nodes['1 comment']->comment = COMMENT_NODE_HIDDEN;
  795. node_save($this->searchable_nodes['1 comment']);
  796. $this->drupalPost('', $edit, t('Search'));
  797. $this->assertNoText(t('0 comments'), t('Empty comment count does not display for nodes with comment status set to Hidden'));
  798. $this->assertNoText(t('1 comment'), t('Non-empty comment count does not display for nodes with comment status set to Hidden'));
  799. }
  800. }
  801. /**
  802. * Test search_simplify() on every Unicode character, and some other cases.
  803. */
  804. class SearchSimplifyTestCase extends DrupalWebTestCase {
  805. public static function getInfo() {
  806. return array(
  807. 'name' => 'Search simplify',
  808. 'description' => 'Check that the search_simply() function works as intended.',
  809. 'group' => 'Search',
  810. );
  811. }
  812. function testSearchSimplifyUnicode() {
  813. $input = file_get_contents(DRUPAL_ROOT . '/modules/search/tests/UnicodeTest.txt');
  814. $strings = explode(chr(10), $input);
  815. foreach ($strings as $key => $string) {
  816. $simplified = search_simplify($string);
  817. if ($key % 2) {
  818. $this->assertIdentical($simplified, ' ', "Line $key is excluded from the index");
  819. }
  820. else {
  821. $this->assertTrue(drupal_strlen($simplified) >= drupal_strlen($string), "Nothing is removed on line $key.");
  822. }
  823. }
  824. $string = '';
  825. for ($i = 0; $i < 32; $i++) {
  826. $string .= chr($i);
  827. }
  828. // Diff really does not like files starting with \0 so test it separately.
  829. $this->assertIdentical(' ', search_simplify($string), t('Search simplify works for ASCII control characters.'));
  830. }
  831. /**
  832. * Tests that search_simplify() does the right thing with punctuation.
  833. */
  834. function testSearchSimplifyPunctuation() {
  835. $cases = array(
  836. array('20.03/94-28,876', '20039428876', 'Punctuation removed from numbers'),
  837. array('great...drupal--module', 'great drupal module', 'Multiple dot and dashes are word boundaries'),
  838. array('very_great-drupal.module', 'verygreatdrupalmodule', 'Single dot, dash, underscore are removed'),
  839. array('regular,punctuation;word', 'regular punctuation word', 'Punctuation is a word boundary'),
  840. );
  841. foreach ($cases as $case) {
  842. $out = trim(search_simplify($case[0]));
  843. $this->assertEqual($out, $case[1], $case[2]);
  844. }
  845. }
  846. }
  847. /**
  848. * Test config page.
  849. */
  850. class SearchConfigSettingsForm extends DrupalWebTestCase {
  851. public $search_user;
  852. public $search_node;
  853. public static function getInfo() {
  854. return array(
  855. 'name' => 'Config settings form',
  856. 'description' => 'Verify the search config settings form.',
  857. 'group' => 'Search',
  858. );
  859. }
  860. function setUp() {
  861. parent::setUp('search', 'search_extra_type');
  862. // Login as a user that can create and search content.
  863. $this->search_user = $this->drupalCreateUser(array('search content', 'administer search', 'administer nodes', 'bypass node access', 'access user profiles', 'administer users', 'administer blocks'));
  864. $this->drupalLogin($this->search_user);
  865. // Add a single piece of content and index it.
  866. $node = $this->drupalCreateNode();
  867. $this->search_node = $node;
  868. // Link the node to itself to test that it's only indexed once. The content
  869. // also needs the word "pizza" so we can use it as the search keyword.
  870. $langcode = LANGUAGE_NONE;
  871. $body_key = "body[$langcode][0][value]";
  872. $edit[$body_key] = l($node->title, 'node/' . $node->nid) . ' pizza sandwich';
  873. $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
  874. node_update_index();
  875. search_update_totals();
  876. // Enable the search block.
  877. $edit = array();
  878. $edit['search_form[region]'] = 'content';
  879. $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
  880. }
  881. /**
  882. * Verify the search settings form.
  883. */
  884. function testSearchSettingsPage() {
  885. // Test that the settings form displays the correct count of items left to index.
  886. $this->drupalGet('admin/config/search/settings');
  887. $this->assertText(t('There are @count items left to index.', array('@count' => 0)));
  888. // Test the re-index button.
  889. $this->drupalPost('admin/config/search/settings', array(), t('Re-index site'));
  890. $this->assertText(t('Are you sure you want to re-index the site'));
  891. $this->drupalPost('admin/config/search/settings/reindex', array(), t('Re-index site'));
  892. $this->assertText(t('The index will be rebuilt'));
  893. $this->drupalGet('admin/config/search/settings');
  894. $this->assertText(t('There is 1 item left to index.'));
  895. }
  896. /**
  897. * Verify that you can disable individual search modules.
  898. */
  899. function testSearchModuleDisabling() {
  900. // Array of search types to test: 'path' is the search path, 'title' is
  901. // the tab title, 'keys' are the keywords to search for, and 'text' is
  902. // the text to assert is on the results page.
  903. $module_info = array(
  904. 'node' => array(
  905. 'path' => 'node',
  906. 'title' => 'Content',
  907. 'keys' => 'pizza',
  908. 'text' => $this->search_node->title,
  909. ),
  910. 'user' => array(
  911. 'path' => 'user',
  912. 'title' => 'User',
  913. 'keys' => $this->search_user->name,
  914. 'text' => $this->search_user->mail,
  915. ),
  916. 'search_extra_type' => array(
  917. 'path' => 'dummy_path',
  918. 'title' => 'Dummy search type',
  919. 'keys' => 'foo',
  920. 'text' => 'Dummy search snippet to display',
  921. ),
  922. );
  923. $modules = array_keys($module_info);
  924. // Test each module if it's enabled as the only search module.
  925. foreach ($modules as $module) {
  926. // Enable the one module and disable other ones.
  927. $info = $module_info[$module];
  928. $edit = array();
  929. foreach ($modules as $other) {
  930. $edit['search_active_modules[' . $other . ']'] = (($other == $module) ? $module : FALSE);
  931. }
  932. $edit['search_default_module'] = $module;
  933. $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration'));
  934. // Run a search from the correct search URL.
  935. $this->drupalGet('search/' . $info['path'] . '/' . $info['keys']);
  936. $this->assertNoText('no results', $info['title'] . ' search found results');
  937. $this->assertText($info['text'], 'Correct search text found');
  938. // Verify that other module search tab titles are not visible.
  939. foreach ($modules as $other) {
  940. if ($other != $module) {
  941. $title = $module_info[$other]['title'];
  942. $this->assertNoText($title, $title . ' search tab is not shown');
  943. }
  944. }
  945. // Run a search from the search block on the node page. Verify you get
  946. // to this module's search results page.
  947. $terms = array('search_block_form' => $info['keys']);
  948. $this->drupalPost('node', $terms, t('Search'));
  949. $this->assertEqual(
  950. $this->getURL(),
  951. url('search/' . $info['path'] . '/' . $info['keys'], array('absolute' => TRUE)),
  952. 'Block redirected to right search page');
  953. // Try an invalid search path. Should redirect to our active module.
  954. $this->drupalGet('search/not_a_module_path');
  955. $this->assertEqual(
  956. $this->getURL(),
  957. url('search/' . $info['path'], array('absolute' => TRUE)),
  958. 'Invalid search path redirected to default search page');
  959. }
  960. // Test with all search modules enabled. When you go to the search
  961. // page or run search, all modules should be shown.
  962. $edit = array();
  963. foreach ($modules as $module) {
  964. $edit['search_active_modules[' . $module . ']'] = $module;
  965. }
  966. $edit['search_default_module'] = 'node';
  967. $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration'));
  968. foreach (array('search/node/pizza', 'search/node') as $path) {
  969. $this->drupalGet($path);
  970. foreach ($modules as $module) {
  971. $title = $module_info[$module]['title'];
  972. $this->assertText($title, $title . ' search tab is shown');
  973. }
  974. }
  975. }
  976. }
  977. /**
  978. * Tests the search_excerpt() function.
  979. */
  980. class SearchExcerptTestCase extends DrupalUnitTestCase {
  981. public static function getInfo() {
  982. return array(
  983. 'name' => 'Search excerpt extraction',
  984. 'description' => 'Tests that the search_excerpt() function works.',
  985. 'group' => 'Search',
  986. );
  987. }
  988. function setUp() {
  989. parent::setUp('search');
  990. }
  991. /**
  992. * Tests search_excerpt() with several simulated search keywords.
  993. *
  994. * Passes keywords and a sample marked up string, "The quick
  995. * brown fox jumps over the lazy dog", and compares it to the
  996. * correctly marked up string. The correctly marked up string
  997. * contains either highlighted keywords or the original marked
  998. * up string if no keywords matched the string.
  999. */
  1000. function testSearchExcerpt() {
  1001. // Make some text with entities and tags.
  1002. $text = 'The <strong>quick</strong> <a href="#">brown</a> fox &amp; jumps <h2>over</h2> the lazy dog';
  1003. // Note: The search_excerpt() function adds some extra spaces -- not
  1004. // important for HTML formatting. Remove these for comparison.
  1005. $expected = 'The quick brown fox &amp; jumps over the lazy dog';
  1006. $result = preg_replace('| +|', ' ', search_excerpt('nothing', $text));
  1007. $this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Entire string is returned when keyword is not found in short string');
  1008. $result = preg_replace('| +|', ' ', search_excerpt('fox', $text));
  1009. $this->assertEqual($result, 'The quick brown <strong>fox</strong> &amp; jumps over the lazy dog ...', 'Found keyword is highlighted');
  1010. $longtext = str_repeat($text . ' ', 10);
  1011. $result = preg_replace('| +|', ' ', search_excerpt('nothing', $text));
  1012. $this->assertTrue(strpos($result, $expected) === 0, 'When keyword is not found in long string, return value starts as expected');
  1013. $entities = str_repeat('k&eacute;sz&iacute;t&eacute;se ', 20);
  1014. $result = preg_replace('| +|', ' ', search_excerpt('nothing', $entities));
  1015. $this->assertFalse(strpos($result, '&'), 'Entities are not present in excerpt');
  1016. $this->assertTrue(strpos($result, 'í') > 0, 'Entities are converted in excerpt');
  1017. }
  1018. }
  1019. /**
  1020. * Test the CJK tokenizer.
  1021. */
  1022. class SearchTokenizerTestCase extends DrupalWebTestCase {
  1023. public static function getInfo() {
  1024. return array(
  1025. 'name' => 'CJK tokenizer',
  1026. 'description' => 'Check that CJK tokenizer works as intended.',
  1027. 'group' => 'Search',
  1028. );
  1029. }
  1030. function setUp() {
  1031. parent::setUp('search');
  1032. }
  1033. /**
  1034. * Verifies that strings of CJK characters are tokenized.
  1035. *
  1036. * The search_simplify() function does special things with numbers, symbols,
  1037. * and punctuation. So we only test that CJK characters that are not in these
  1038. * character classes are tokenized properly. See PREG_CLASS_CKJ for more
  1039. * information.
  1040. */
  1041. function testTokenizer() {
  1042. // Set the minimum word size to 1 (to split all CJK characters) and make
  1043. // sure CJK tokenizing is turned on.
  1044. variable_set('minimum_word_size', 1);
  1045. variable_set('overlap_cjk', TRUE);
  1046. $this->refreshVariables();
  1047. // Create a string of CJK characters from various character ranges in
  1048. // the Unicode tables.
  1049. // Beginnings of the character ranges.
  1050. $starts = array(
  1051. 'CJK unified' => 0x4e00,
  1052. 'CJK Ext A' => 0x3400,
  1053. 'CJK Compat' => 0xf900,
  1054. 'Hangul Jamo' => 0x1100,
  1055. 'Hangul Ext A' => 0xa960,
  1056. 'Hangul Ext B' => 0xd7b0,
  1057. 'Hangul Compat' => 0x3131,
  1058. 'Half non-punct 1' => 0xff21,
  1059. 'Half non-punct 2' => 0xff41,
  1060. 'Half non-punct 3' => 0xff66,
  1061. 'Hangul Syllables' => 0xac00,
  1062. 'Hiragana' => 0x3040,
  1063. 'Katakana' => 0x30a1,
  1064. 'Katakana Ext' => 0x31f0,
  1065. 'CJK Reserve 1' => 0x20000,
  1066. 'CJK Reserve 2' => 0x30000,
  1067. 'Bomofo' => 0x3100,
  1068. 'Bomofo Ext' => 0x31a0,
  1069. 'Lisu' => 0xa4d0,
  1070. 'Yi' => 0xa000,
  1071. );
  1072. // Ends of the character ranges.
  1073. $ends = array(
  1074. 'CJK unified' => 0x9fcf,
  1075. 'CJK Ext A' => 0x4dbf,
  1076. 'CJK Compat' => 0xfaff,
  1077. 'Hangul Jamo' => 0x11ff,
  1078. 'Hangul Ext A' => 0xa97f,
  1079. 'Hangul Ext B' => 0xd7ff,
  1080. 'Hangul Compat' => 0x318e,
  1081. 'Half non-punct 1' => 0xff3a,
  1082. 'Half non-punct 2' => 0xff5a,
  1083. 'Half non-punct 3' => 0xffdc,
  1084. 'Hangul Syllables' => 0xd7af,
  1085. 'Hiragana' => 0x309f,
  1086. 'Katakana' => 0x30ff,
  1087. 'Katakana Ext' => 0x31ff,
  1088. 'CJK Reserve 1' => 0x2fffd,
  1089. 'CJK Reserve 2' => 0x3fffd,
  1090. 'Bomofo' => 0x312f,
  1091. 'Bomofo Ext' => 0x31b7,
  1092. 'Lisu' => 0xa4fd,
  1093. 'Yi' => 0xa48f,
  1094. );
  1095. // Generate characters consisting of starts, midpoints, and ends.
  1096. $chars = array();
  1097. $charcodes = array();
  1098. foreach ($starts as $key => $value) {
  1099. $charcodes[] = $starts[$key];
  1100. $chars[] = $this->code2utf($starts[$key]);
  1101. $mid = round(0.5 * ($starts[$key] + $ends[$key]));
  1102. $charcodes[] = $mid;
  1103. $chars[] = $this->code2utf($mid);
  1104. $charcodes[] = $ends[$key];
  1105. $chars[] = $this->code2utf($ends[$key]);
  1106. }
  1107. // Merge into a string and tokenize.
  1108. $string = implode('', $chars);
  1109. $out = trim(search_simplify($string));
  1110. $expected = drupal_strtolower(implode(' ', $chars));
  1111. // Verify that the output matches what we expect.
  1112. $this->assertEqual($out, $expected, 'CJK tokenizer worked on all supplied CJK characters');
  1113. }
  1114. /**
  1115. * Verifies that strings of non-CJK characters are not tokenized.
  1116. *
  1117. * This is just a sanity check - it verifies that strings of letters are
  1118. * not tokenized.
  1119. */
  1120. function testNoTokenizer() {
  1121. // Set the minimum word size to 1 (to split all CJK characters) and make
  1122. // sure CJK tokenizing is turned on.
  1123. variable_set('minimum_word_size', 1);
  1124. variable_set('overlap_cjk', TRUE);
  1125. $this->refreshVariables();
  1126. $letters = 'abcdefghijklmnopqrstuvwxyz';
  1127. $out = trim(search_simplify($letters));
  1128. $this->assertEqual($letters, $out, 'Letters are not CJK tokenized');
  1129. }
  1130. /**
  1131. * Like PHP chr() function, but for unicode characters.
  1132. *
  1133. * chr() only works for ASCII characters up to character 255. This function
  1134. * converts a number to the corresponding unicode character. Adapted from
  1135. * functions supplied in comments on several functions on php.net.
  1136. */
  1137. function code2utf($num) {
  1138. if ($num < 128) {
  1139. return chr($num);
  1140. }
  1141. if ($num < 2048) {
  1142. return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
  1143. }
  1144. if ($num < 65536) {
  1145. return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
  1146. }
  1147. if ($num < 2097152) {
  1148. return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
  1149. }
  1150. return '';
  1151. }
  1152. }