See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Simple db search engine tests. 19 * 20 * @package search_simpledb 21 * @category test 22 * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 global $CFG; 29 require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php'); 30 require_once($CFG->dirroot . '/search/tests/fixtures/mock_search_area.php'); 31 32 /** 33 * Simple search engine base unit tests. 34 * 35 * @package search_simpledb 36 * @category test 37 * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40 class search_simpledb_engine_testcase extends advanced_testcase { 41 42 /** 43 * @var \core_search::manager 44 */ 45 protected $search = null; 46 47 /** 48 * @var \ 49 */ 50 protected $engine = null; 51 52 /** 53 * @var core_search_generator 54 */ 55 protected $generator = null; 56 57 /** 58 * Initial stuff. 59 * 60 * @return void 61 */ 62 public function setUp() { 63 $this->resetAfterTest(); 64 65 if ($this->requires_manual_index_update()) { 66 // We need to update fulltext index manually, which requires an alter table statement. 67 $this->preventResetByRollback(); 68 } 69 70 set_config('enableglobalsearch', true); 71 72 // Inject search_simpledb engine into the testable core search as we need to add the mock 73 // search component to it. 74 75 $this->engine = new \search_simpledb\engine(); 76 $this->search = testable_core_search::instance($this->engine); 77 78 $this->generator = self::getDataGenerator()->get_plugin_generator('core_search'); 79 $this->generator->setup(); 80 81 $this->setAdminUser(); 82 } 83 84 /** 85 * tearDown 86 * 87 * @return void 88 */ 89 public function tearDown() { 90 // For unit tests before PHP 7, teardown is called even on skip. So only do our teardown if we did setup. 91 if ($this->generator) { 92 // Moodle DML freaks out if we don't teardown the temp table after each run. 93 $this->generator->teardown(); 94 $this->generator = null; 95 } 96 } 97 98 /** 99 * Test indexing process. 100 * 101 * @return void 102 */ 103 public function test_index() { 104 global $DB; 105 106 $this->add_mock_search_area(); 107 108 $record = new \stdClass(); 109 $record->timemodified = time() - 1; 110 $this->generator->create_record($record); 111 112 // Data gets into the search engine. 113 $this->assertTrue($this->search->index()); 114 115 // Not anymore as everything was already added. 116 sleep(1); 117 $this->assertFalse($this->search->index()); 118 119 $this->generator->create_record(); 120 121 // Indexing again once there is new data. 122 $this->assertTrue($this->search->index()); 123 } 124 125 /** 126 * Test search filters. 127 * 128 * @return void 129 */ 130 public function test_search() { 131 global $USER, $DB; 132 133 $this->add_mock_search_area(); 134 135 $this->generator->create_record(); 136 $record = new \stdClass(); 137 $record->title = "Special title"; 138 $this->generator->create_record($record); 139 140 $this->search->index(); 141 $this->update_index(); 142 143 $querydata = new stdClass(); 144 $querydata->q = 'message'; 145 $results = $this->search->search($querydata); 146 $this->assertCount(2, $results); 147 148 // Based on core_mocksearch\search\indexer. 149 $this->assertEquals($USER->id, $results[0]->get('userid')); 150 $this->assertEquals(\context_course::instance(SITEID)->id, $results[0]->get('contextid')); 151 152 // Do a test to make sure we aren't searching non-query fields, like areaid. 153 $querydata->q = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 154 $this->assertCount(0, $this->search->search($querydata)); 155 $querydata->q = 'message'; 156 157 sleep(1); 158 $beforeadding = time(); 159 sleep(1); 160 $this->generator->create_record(); 161 $this->search->index(); 162 $this->update_index(); 163 164 // Timestart. 165 $querydata->timestart = $beforeadding; 166 $this->assertCount(1, $this->search->search($querydata)); 167 168 // Timeend. 169 unset($querydata->timestart); 170 $querydata->timeend = $beforeadding; 171 $this->assertCount(2, $this->search->search($querydata)); 172 173 // Title. 174 unset($querydata->timeend); 175 $querydata->title = 'Special title'; 176 $this->assertCount(1, $this->search->search($querydata)); 177 178 // Course IDs. 179 unset($querydata->title); 180 $querydata->courseids = array(SITEID + 1); 181 $this->assertCount(0, $this->search->search($querydata)); 182 183 $querydata->courseids = array(SITEID); 184 $this->assertCount(3, $this->search->search($querydata)); 185 186 // Now try some area-id combinations. 187 unset($querydata->courseids); 188 $forumpostareaid = \core_search\manager::generate_areaid('mod_forum', 'post'); 189 $mockareaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 190 191 $querydata->areaids = array($forumpostareaid); 192 $this->assertCount(0, $this->search->search($querydata)); 193 194 $querydata->areaids = array($forumpostareaid, $mockareaid); 195 $this->assertCount(3, $this->search->search($querydata)); 196 197 $querydata->areaids = array($mockareaid); 198 $this->assertCount(3, $this->search->search($querydata)); 199 200 $querydata->areaids = array(); 201 $this->assertCount(3, $this->search->search($querydata)); 202 203 // Check that index contents get updated. 204 $this->generator->delete_all(); 205 $this->search->index(true); 206 $this->update_index(); 207 unset($querydata->title); 208 $querydata->q = ''; 209 $this->assertCount(0, $this->search->search($querydata)); 210 } 211 212 /** 213 * Test delete function 214 * 215 * @return void 216 */ 217 public function test_delete() { 218 219 $this->add_mock_search_area(); 220 221 $this->generator->create_record(); 222 $this->generator->create_record(); 223 $this->search->index(); 224 $this->update_index(); 225 226 $querydata = new stdClass(); 227 $querydata->q = 'message'; 228 229 $this->assertCount(2, $this->search->search($querydata)); 230 231 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 232 $this->search->delete_index($areaid); 233 $this->update_index(); 234 $this->assertCount(0, $this->search->search($querydata)); 235 } 236 237 /** 238 * Test user is allowed. 239 * 240 * @return void 241 */ 242 public function test_alloweduserid() { 243 244 $this->add_mock_search_area(); 245 246 $area = new core_mocksearch\search\mock_search_area(); 247 248 $record = $this->generator->create_record(); 249 250 // Get the doc and insert the default doc. 251 $doc = $area->get_document($record); 252 $this->engine->add_document($doc); 253 254 $users = array(); 255 $users[] = $this->getDataGenerator()->create_user(); 256 $users[] = $this->getDataGenerator()->create_user(); 257 $users[] = $this->getDataGenerator()->create_user(); 258 259 // Add a record that only user 100 can see. 260 $originalid = $doc->get('id'); 261 262 // Now add a custom doc for each user. 263 foreach ($users as $user) { 264 $doc = $area->get_document($record); 265 $doc->set('id', $originalid.'-'.$user->id); 266 $doc->set('owneruserid', $user->id); 267 $this->engine->add_document($doc); 268 } 269 $this->update_index(); 270 271 $this->engine->area_index_complete($area->get_area_id()); 272 273 $querydata = new stdClass(); 274 $querydata->q = 'message'; 275 $querydata->title = $doc->get('title'); 276 277 // We are going to go through each user and see if they get the original and the owned doc. 278 foreach ($users as $user) { 279 $this->setUser($user); 280 281 $results = $this->search->search($querydata); 282 $this->assertCount(2, $results); 283 284 $owned = 0; 285 $notowned = 0; 286 287 // We don't know what order we will get the results in, so we are doing this. 288 foreach ($results as $result) { 289 $owneruserid = $result->get('owneruserid'); 290 if (empty($owneruserid)) { 291 $notowned++; 292 $this->assertEquals(0, $owneruserid); 293 $this->assertEquals($originalid, $result->get('id')); 294 } else { 295 $owned++; 296 $this->assertEquals($user->id, $owneruserid); 297 $this->assertEquals($originalid.'-'.$user->id, $result->get('id')); 298 } 299 } 300 301 $this->assertEquals(1, $owned); 302 $this->assertEquals(1, $notowned); 303 } 304 305 // Now test a user with no owned results. 306 $otheruser = $this->getDataGenerator()->create_user(); 307 $this->setUser($otheruser); 308 309 $results = $this->search->search($querydata); 310 $this->assertCount(1, $results); 311 312 $this->assertEquals(0, $results[0]->get('owneruserid')); 313 $this->assertEquals($originalid, $results[0]->get('id')); 314 } 315 316 public function test_delete_by_id() { 317 318 $this->add_mock_search_area(); 319 320 $this->generator->create_record(); 321 $this->generator->create_record(); 322 $this->search->index(); 323 $this->update_index(); 324 325 $querydata = new stdClass(); 326 327 // Then search to make sure they are there. 328 $querydata->q = 'message'; 329 $results = $this->search->search($querydata); 330 $this->assertCount(2, $results); 331 332 $first = reset($results); 333 $deleteid = $first->get('id'); 334 335 $this->engine->delete_by_id($deleteid); 336 $this->update_index(); 337 338 // Check that we don't get a result for it anymore. 339 $results = $this->search->search($querydata); 340 $this->assertCount(1, $results); 341 $result = reset($results); 342 $this->assertNotEquals($deleteid, $result->get('id')); 343 } 344 345 /** 346 * Tries out deleting data for a context or a course. 347 * 348 * @throws moodle_exception 349 */ 350 public function test_deleted_contexts_and_courses() { 351 // Create some courses and activities. 352 $generator = $this->getDataGenerator(); 353 $course1 = $generator->create_course(['fullname' => 'C1', 'summary' => 'xyzzy']); 354 $course1page1 = $generator->create_module('page', ['course' => $course1, 'name' => 'C1P1', 'content' => 'xyzzy']); 355 $generator->create_module('page', ['course' => $course1, 'name' => 'C1P2', 'content' => 'xyzzy']); 356 $course2 = $generator->create_course(['fullname' => 'C2', 'summary' => 'xyzzy']); 357 $course2page = $generator->create_module('page', ['course' => $course2, 'name' => 'C2P', 'content' => 'xyzzy']); 358 $course2pagecontext = \context_module::instance($course2page->cmid); 359 360 $this->search->index(); 361 362 // By default we have all data in the index. 363 $this->assert_raw_index_contents('xyzzy', ['C1', 'C1P1', 'C1P2', 'C2', 'C2P']); 364 365 // Say we delete the course2pagecontext... 366 $this->engine->delete_index_for_context($course2pagecontext->id); 367 $this->assert_raw_index_contents('xyzzy', ['C1', 'C1P1', 'C1P2', 'C2']); 368 369 // Now delete the second course... 370 $this->engine->delete_index_for_course($course2->id); 371 $this->assert_raw_index_contents('xyzzy', ['C1', 'C1P1', 'C1P2']); 372 373 // Finally let's delete using Moodle functions to check that works. Single context first. 374 course_delete_module($course1page1->cmid); 375 $this->assert_raw_index_contents('xyzzy', ['C1', 'C1P2']); 376 delete_course($course1, false); 377 $this->assert_raw_index_contents('xyzzy', []); 378 } 379 380 /** 381 * Check the contents of the index. 382 * 383 * @param string $searchword Word to match within the content field 384 * @param string[] $expected Array of expected result titles, in alphabetical order 385 * @throws dml_exception 386 */ 387 protected function assert_raw_index_contents(string $searchword, array $expected) { 388 global $DB; 389 $results = $DB->get_records_select('search_simpledb_index', 390 $DB->sql_like('content', '?'), ['%' . $searchword . '%'], "id, {$DB->sql_order_by_text('title')}"); 391 $titles = array_map(function($x) { 392 return $x->title; 393 }, $results); 394 sort($titles); 395 $this->assertEquals($expected, $titles); 396 } 397 398 /** 399 * Adds a mock search area to the search system. 400 */ 401 protected function add_mock_search_area() { 402 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 403 $this->search->add_search_area($areaid, new core_mocksearch\search\mock_search_area()); 404 } 405 406 /** 407 * Updates mssql fulltext index if necessary. 408 * 409 * @return bool 410 */ 411 private function update_index() { 412 global $DB; 413 414 if (!$this->requires_manual_index_update()) { 415 return; 416 } 417 418 $DB->execute("ALTER FULLTEXT INDEX ON {search_simpledb_index} START UPDATE POPULATION"); 419 420 $catalogname = $DB->get_prefix() . 'search_simpledb_catalog'; 421 $retries = 0; 422 do { 423 // 0.2 seconds. 424 usleep(200000); 425 426 $record = $DB->get_record_sql("SELECT FULLTEXTCATALOGPROPERTY(cat.name, 'PopulateStatus') AS [PopulateStatus] 427 FROM sys.fulltext_catalogs AS cat 428 WHERE cat.name = ?", array($catalogname)); 429 $retries++; 430 431 } while ($retries < 100 && $record->populatestatus != '0'); 432 433 if ($retries === 100) { 434 // No update after 20 seconds... 435 $this->fail('Sorry, your SQL server fulltext search index is too slow.'); 436 } 437 } 438 439 /** 440 * Mssql with fulltext support requires manual updates. 441 * 442 * @return bool 443 */ 444 private function requires_manual_index_update() { 445 global $DB; 446 return ($DB->get_dbfamily() === 'mssql' && $DB->is_fulltext_search_supported()); 447 } 448 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body