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