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 * Solr earch engine base unit tests. 19 * 20 * Required params: 21 * - define('TEST_SEARCH_SOLR_HOSTNAME', '127.0.0.1'); 22 * - define('TEST_SEARCH_SOLR_PORT', '8983'); 23 * - define('TEST_SEARCH_SOLR_INDEXNAME', 'unittest'); 24 * 25 * Optional params: 26 * - define('TEST_SEARCH_SOLR_USERNAME', ''); 27 * - define('TEST_SEARCH_SOLR_PASSWORD', ''); 28 * - define('TEST_SEARCH_SOLR_SSLCERT', ''); 29 * - define('TEST_SEARCH_SOLR_SSLKEY', ''); 30 * - define('TEST_SEARCH_SOLR_KEYPASSWORD', ''); 31 * - define('TEST_SEARCH_SOLR_CAINFOCERT', ''); 32 * 33 * @package search_solr 34 * @category phpunit 35 * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 39 defined('MOODLE_INTERNAL') || die(); 40 41 global $CFG; 42 require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php'); 43 require_once($CFG->dirroot . '/search/tests/fixtures/mock_search_area.php'); 44 require_once($CFG->dirroot . '/search/engine/solr/tests/fixtures/testable_engine.php'); 45 46 /** 47 * Solr search engine base unit tests. 48 * 49 * @package search_solr 50 * @category phpunit 51 * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} 52 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 53 */ 54 class search_solr_engine_testcase extends advanced_testcase { 55 56 /** 57 * @var \core_search\manager 58 */ 59 protected $search = null; 60 61 /** 62 * @var Instace of core_search_generator. 63 */ 64 protected $generator = null; 65 66 /** 67 * @var Instace of testable_engine. 68 */ 69 protected $engine = null; 70 71 public function setUp() { 72 $this->resetAfterTest(); 73 set_config('enableglobalsearch', true); 74 set_config('searchengine', 'solr'); 75 76 if (!function_exists('solr_get_version')) { 77 $this->markTestSkipped('Solr extension is not loaded.'); 78 } 79 80 if (!defined('TEST_SEARCH_SOLR_HOSTNAME') || !defined('TEST_SEARCH_SOLR_INDEXNAME') || 81 !defined('TEST_SEARCH_SOLR_PORT')) { 82 $this->markTestSkipped('Solr extension test server not set.'); 83 } 84 85 set_config('server_hostname', TEST_SEARCH_SOLR_HOSTNAME, 'search_solr'); 86 set_config('server_port', TEST_SEARCH_SOLR_PORT, 'search_solr'); 87 set_config('indexname', TEST_SEARCH_SOLR_INDEXNAME, 'search_solr'); 88 89 if (defined('TEST_SEARCH_SOLR_USERNAME')) { 90 set_config('server_username', TEST_SEARCH_SOLR_USERNAME, 'search_solr'); 91 } 92 93 if (defined('TEST_SEARCH_SOLR_PASSWORD')) { 94 set_config('server_password', TEST_SEARCH_SOLR_PASSWORD, 'search_solr'); 95 } 96 97 if (defined('TEST_SEARCH_SOLR_SSLCERT')) { 98 set_config('secure', true, 'search_solr'); 99 set_config('ssl_cert', TEST_SEARCH_SOLR_SSLCERT, 'search_solr'); 100 } 101 102 if (defined('TEST_SEARCH_SOLR_SSLKEY')) { 103 set_config('ssl_key', TEST_SEARCH_SOLR_SSLKEY, 'search_solr'); 104 } 105 106 if (defined('TEST_SEARCH_SOLR_KEYPASSWORD')) { 107 set_config('ssl_keypassword', TEST_SEARCH_SOLR_KEYPASSWORD, 'search_solr'); 108 } 109 110 if (defined('TEST_SEARCH_SOLR_CAINFOCERT')) { 111 set_config('ssl_cainfo', TEST_SEARCH_SOLR_CAINFOCERT, 'search_solr'); 112 } 113 114 set_config('fileindexing', 1, 'search_solr'); 115 116 // We are only test indexing small string files, so setting this as low as we can. 117 set_config('maxindexfilekb', 1, 'search_solr'); 118 119 $this->generator = self::getDataGenerator()->get_plugin_generator('core_search'); 120 $this->generator->setup(); 121 122 // Inject search solr engine into the testable core search as we need to add the mock 123 // search component to it. 124 $this->engine = new \search_solr\testable_engine(); 125 $this->search = testable_core_search::instance($this->engine); 126 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 127 $this->search->add_search_area($areaid, new core_mocksearch\search\mock_search_area()); 128 129 $this->setAdminUser(); 130 131 // Cleanup before doing anything on it as the index it is out of this test control. 132 $this->search->delete_index(); 133 134 // Add moodle fields if they don't exist. 135 $schema = new \search_solr\schema(); 136 $schema->setup(false); 137 } 138 139 public function tearDown() { 140 // For unit tests before PHP 7, teardown is called even on skip. So only do our teardown if we did setup. 141 if ($this->generator) { 142 // Moodle DML freaks out if we don't teardown the temp table after each run. 143 $this->generator->teardown(); 144 $this->generator = null; 145 } 146 } 147 148 /** 149 * Simple data provider to allow tests to be run with file indexing on and off. 150 */ 151 public function file_indexing_provider() { 152 return array( 153 'file-indexing-on' => array(1), 154 'file-indexing-off' => array(0) 155 ); 156 } 157 158 public function test_connection() { 159 $this->assertTrue($this->engine->is_server_ready()); 160 } 161 162 /** 163 * @dataProvider file_indexing_provider 164 */ 165 public function test_index($fileindexing) { 166 global $DB; 167 168 $this->engine->test_set_config('fileindexing', $fileindexing); 169 170 $record = new \stdClass(); 171 $record->timemodified = time() - 1; 172 $this->generator->create_record($record); 173 174 // Data gets into the search engine. 175 $this->assertTrue($this->search->index()); 176 177 // Not anymore as everything was already added. 178 sleep(1); 179 $this->assertFalse($this->search->index()); 180 181 $this->generator->create_record(); 182 183 // Indexing again once there is new data. 184 $this->assertTrue($this->search->index()); 185 } 186 187 /** 188 * Better keep this not very strict about which or how many results are returned as may depend on solr engine config. 189 * 190 * @dataProvider file_indexing_provider 191 * 192 * @return void 193 */ 194 public function test_search($fileindexing) { 195 global $USER, $DB; 196 197 $this->engine->test_set_config('fileindexing', $fileindexing); 198 199 $this->generator->create_record(); 200 $record = new \stdClass(); 201 $record->title = "Special title"; 202 $this->generator->create_record($record); 203 204 $this->search->index(); 205 206 $querydata = new stdClass(); 207 $querydata->q = 'message'; 208 $results = $this->search->search($querydata); 209 $this->assertCount(2, $results); 210 211 // Based on core_mocksearch\search\indexer. 212 $this->assertEquals($USER->id, $results[0]->get('userid')); 213 $this->assertEquals(\context_course::instance(SITEID)->id, $results[0]->get('contextid')); 214 215 // Do a test to make sure we aren't searching non-query fields, like areaid. 216 $querydata->q = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 217 $this->assertCount(0, $this->search->search($querydata)); 218 $querydata->q = 'message'; 219 220 sleep(1); 221 $beforeadding = time(); 222 sleep(1); 223 $this->generator->create_record(); 224 $this->search->index(); 225 226 // Timestart. 227 $querydata->timestart = $beforeadding; 228 $this->assertCount(1, $this->search->search($querydata)); 229 230 // Timeend. 231 unset($querydata->timestart); 232 $querydata->timeend = $beforeadding; 233 $this->assertCount(2, $this->search->search($querydata)); 234 235 // Title. 236 unset($querydata->timeend); 237 $querydata->title = 'Special title'; 238 $this->assertCount(1, $this->search->search($querydata)); 239 240 // Course IDs. 241 unset($querydata->title); 242 $querydata->courseids = array(SITEID + 1); 243 $this->assertCount(0, $this->search->search($querydata)); 244 245 $querydata->courseids = array(SITEID); 246 $this->assertCount(3, $this->search->search($querydata)); 247 248 // Now try some area-id combinations. 249 unset($querydata->courseids); 250 $forumpostareaid = \core_search\manager::generate_areaid('mod_forum', 'post'); 251 $mockareaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 252 253 $querydata->areaids = array($forumpostareaid); 254 $this->assertCount(0, $this->search->search($querydata)); 255 256 $querydata->areaids = array($forumpostareaid, $mockareaid); 257 $this->assertCount(3, $this->search->search($querydata)); 258 259 $querydata->areaids = array($mockareaid); 260 $this->assertCount(3, $this->search->search($querydata)); 261 262 $querydata->areaids = array(); 263 $this->assertCount(3, $this->search->search($querydata)); 264 265 // Check that index contents get updated. 266 $this->generator->delete_all(); 267 $this->search->index(true); 268 unset($querydata->title); 269 $querydata->q = '*'; 270 $this->assertCount(0, $this->search->search($querydata)); 271 } 272 273 /** 274 * @dataProvider file_indexing_provider 275 */ 276 public function test_delete($fileindexing) { 277 $this->engine->test_set_config('fileindexing', $fileindexing); 278 279 $this->generator->create_record(); 280 $this->generator->create_record(); 281 $this->search->index(); 282 283 $querydata = new stdClass(); 284 $querydata->q = 'message'; 285 286 $this->assertCount(2, $this->search->search($querydata)); 287 288 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 289 $this->search->delete_index($areaid); 290 $this->assertCount(0, $this->search->search($querydata)); 291 } 292 293 /** 294 * @dataProvider file_indexing_provider 295 */ 296 public function test_alloweduserid($fileindexing) { 297 $this->engine->test_set_config('fileindexing', $fileindexing); 298 299 $area = new core_mocksearch\search\mock_search_area(); 300 301 $record = $this->generator->create_record(); 302 303 // Get the doc and insert the default doc. 304 $doc = $area->get_document($record); 305 $this->engine->add_document($doc); 306 307 $users = array(); 308 $users[] = $this->getDataGenerator()->create_user(); 309 $users[] = $this->getDataGenerator()->create_user(); 310 $users[] = $this->getDataGenerator()->create_user(); 311 312 // Add a record that only user 100 can see. 313 $originalid = $doc->get('id'); 314 315 // Now add a custom doc for each user. 316 foreach ($users as $user) { 317 $doc = $area->get_document($record); 318 $doc->set('id', $originalid.'-'.$user->id); 319 $doc->set('owneruserid', $user->id); 320 $this->engine->add_document($doc); 321 } 322 323 $this->engine->area_index_complete($area->get_area_id()); 324 325 $querydata = new stdClass(); 326 $querydata->q = 'message'; 327 $querydata->title = $doc->get('title'); 328 329 // We are going to go through each user and see if they get the original and the owned doc. 330 foreach ($users as $user) { 331 $this->setUser($user); 332 333 $results = $this->search->search($querydata); 334 $this->assertCount(2, $results); 335 336 $owned = 0; 337 $notowned = 0; 338 339 // We don't know what order we will get the results in, so we are doing this. 340 foreach ($results as $result) { 341 $owneruserid = $result->get('owneruserid'); 342 if (empty($owneruserid)) { 343 $notowned++; 344 $this->assertEquals(0, $owneruserid); 345 $this->assertEquals($originalid, $result->get('id')); 346 } else { 347 $owned++; 348 $this->assertEquals($user->id, $owneruserid); 349 $this->assertEquals($originalid.'-'.$user->id, $result->get('id')); 350 } 351 } 352 353 $this->assertEquals(1, $owned); 354 $this->assertEquals(1, $notowned); 355 } 356 357 // Now test a user with no owned results. 358 $otheruser = $this->getDataGenerator()->create_user(); 359 $this->setUser($otheruser); 360 361 $results = $this->search->search($querydata); 362 $this->assertCount(1, $results); 363 364 $this->assertEquals(0, $results[0]->get('owneruserid')); 365 $this->assertEquals($originalid, $results[0]->get('id')); 366 } 367 368 /** 369 * @dataProvider file_indexing_provider 370 */ 371 public function test_highlight($fileindexing) { 372 global $PAGE; 373 374 $this->engine->test_set_config('fileindexing', $fileindexing); 375 376 $this->generator->create_record(); 377 $this->search->index(); 378 379 $querydata = new stdClass(); 380 $querydata->q = 'message'; 381 382 $results = $this->search->search($querydata); 383 $this->assertCount(1, $results); 384 385 $result = reset($results); 386 387 $regex = '|'.\search_solr\engine::HIGHLIGHT_START.'message'.\search_solr\engine::HIGHLIGHT_END.'|'; 388 $this->assertRegExp($regex, $result->get('content')); 389 390 $searchrenderer = $PAGE->get_renderer('core_search'); 391 $exported = $result->export_for_template($searchrenderer); 392 393 $regex = '|<span class="highlight">message</span>|'; 394 $this->assertRegExp($regex, $exported['content']); 395 } 396 397 public function test_export_file_for_engine() { 398 // Get area to work with. 399 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 400 $area = \core_search\manager::get_search_area($areaid); 401 402 $record = $this->generator->create_record(); 403 404 $doc = $area->get_document($record); 405 $filerecord = new stdClass(); 406 $filerecord->timemodified = 978310800; 407 $file = $this->generator->create_file($filerecord); 408 $doc->add_stored_file($file); 409 410 $filearray = $doc->export_file_for_engine($file); 411 412 $this->assertEquals(\core_search\manager::TYPE_FILE, $filearray['type']); 413 $this->assertEquals($file->get_id(), $filearray['solr_fileid']); 414 $this->assertEquals($file->get_contenthash(), $filearray['solr_filecontenthash']); 415 $this->assertEquals(\search_solr\document::INDEXED_FILE_TRUE, $filearray['solr_fileindexstatus']); 416 $this->assertEquals($file->get_filename(), $filearray['title']); 417 $this->assertEquals(978310800, \search_solr\document::import_time_from_engine($filearray['modified'])); 418 } 419 420 public function test_index_file() { 421 // Very simple test. 422 $file = $this->generator->create_file(); 423 424 $record = new \stdClass(); 425 $record->attachfileids = array($file->get_id()); 426 $this->generator->create_record($record); 427 428 $this->search->index(); 429 $querydata = new stdClass(); 430 $querydata->q = '"File contents"'; 431 432 $this->assertCount(1, $this->search->search($querydata)); 433 } 434 435 public function test_reindexing_files() { 436 // Get area to work with. 437 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 438 $area = \core_search\manager::get_search_area($areaid); 439 440 $record = $this->generator->create_record(); 441 442 $doc = $area->get_document($record); 443 444 // Now we are going to make some files. 445 $fs = get_file_storage(); 446 $syscontext = \context_system::instance(); 447 448 $files = array(); 449 450 $filerecord = new \stdClass(); 451 // We make enough so that we pass the 500 files threashold. That is the boundary when getting files. 452 $boundary = 500; 453 $top = (int)($boundary * 1.1); 454 for ($i = 0; $i < $top; $i++) { 455 $filerecord->filename = 'searchfile'.$i; 456 $filerecord->content = 'Some FileContents'.$i; 457 $file = $this->generator->create_file($filerecord); 458 $doc->add_stored_file($file); 459 $files[] = $file; 460 } 461 462 // Add the doc with lots of files, then commit. 463 $this->engine->add_document($doc, true); 464 $this->engine->area_index_complete($area->get_area_id()); 465 466 // Indexes we are going to check. 0 means we will delete, 1 means we will keep. 467 $checkfiles = array( 468 0 => 0, // Check the begining of the set. 469 1 => 1, 470 2 => 0, 471 ($top - 3) => 0, // Check the end of the set. 472 ($top - 2) => 1, 473 ($top - 1) => 0, 474 ($boundary - 2) => 0, // Check at the boundary between fetch groups. 475 ($boundary - 1) => 0, 476 $boundary => 0, 477 ($boundary + 1) => 0, 478 ((int)($boundary * 0.5)) => 1, // Make sure we keep some middle ones. 479 ((int)($boundary * 1.05)) => 1 480 ); 481 482 $querydata = new stdClass(); 483 484 // First, check that all the files are currently there. 485 foreach ($checkfiles as $key => $unused) { 486 $querydata->q = 'FileContents'.$key; 487 $this->assertCount(1, $this->search->search($querydata)); 488 $querydata->q = 'searchfile'.$key; 489 $this->assertCount(1, $this->search->search($querydata)); 490 } 491 492 // Remove the files we want removed from the files array. 493 foreach ($checkfiles as $key => $keep) { 494 if (!$keep) { 495 unset($files[$key]); 496 } 497 } 498 499 // And make us a new file to add. 500 $filerecord->filename = 'searchfileNew'; 501 $filerecord->content = 'Some FileContentsNew'; 502 $files[] = $this->generator->create_file($filerecord); 503 $checkfiles['New'] = 1; 504 505 $doc = $area->get_document($record); 506 foreach($files as $file) { 507 $doc->add_stored_file($file); 508 } 509 510 // Reindex the document with the changed files. 511 $this->engine->add_document($doc, true); 512 $this->engine->area_index_complete($area->get_area_id()); 513 514 // Go through our check array, and see if the file is there or not. 515 foreach ($checkfiles as $key => $keep) { 516 $querydata->q = 'FileContents'.$key; 517 $this->assertCount($keep, $this->search->search($querydata)); 518 $querydata->q = 'searchfile'.$key; 519 $this->assertCount($keep, $this->search->search($querydata)); 520 } 521 522 // Now check that we get one result when we search from something in all of them. 523 $querydata->q = 'Some'; 524 $this->assertCount(1, $this->search->search($querydata)); 525 } 526 527 /** 528 * Test indexing a file we don't consider indexable. 529 */ 530 public function test_index_filtered_file() { 531 // Get area to work with. 532 $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area'); 533 $area = \core_search\manager::get_search_area($areaid); 534 535 // Get a single record to make a doc from. 536 $record = $this->generator->create_record(); 537 538 $doc = $area->get_document($record); 539 540 // Now we are going to make some files. 541 $fs = get_file_storage(); 542 $syscontext = \context_system::instance(); 543 544 // We need to make a file greater than 1kB in size, which is the lowest filter size. 545 $filerecord = new \stdClass(); 546 $filerecord->filename = 'largefile'; 547 $filerecord->content = 'Some LargeFindContent to find.'; 548 for ($i = 0; $i < 200; $i++) { 549 $filerecord->content .= ' The quick brown fox jumps over the lazy dog.'; 550 } 551 552 $this->assertGreaterThan(1024, strlen($filerecord->content)); 553 554 $file = $this->generator->create_file($filerecord); 555 $doc->add_stored_file($file); 556 557 $filerecord->filename = 'smallfile'; 558 $filerecord->content = 'Some SmallFindContent to find.'; 559 $file = $this->generator->create_file($filerecord); 560 $doc->add_stored_file($file); 561 562 $this->engine->add_document($doc, true); 563 $this->engine->area_index_complete($area->get_area_id()); 564 565 $querydata = new stdClass(); 566 // We shouldn't be able to find the large file contents. 567 $querydata->q = 'LargeFindContent'; 568 $this->assertCount(0, $this->search->search($querydata)); 569 570 // But we should be able to find the filename. 571 $querydata->q = 'largefile'; 572 $this->assertCount(1, $this->search->search($querydata)); 573 574 // We should be able to find the small file contents. 575 $querydata->q = 'SmallFindContent'; 576 $this->assertCount(1, $this->search->search($querydata)); 577 578 // And we should be able to find the filename. 579 $querydata->q = 'smallfile'; 580 $this->assertCount(1, $this->search->search($querydata)); 581 } 582 583 public function test_delete_by_id() { 584 // First get files in the index. 585 $file = $this->generator->create_file(); 586 $record = new \stdClass(); 587 $record->attachfileids = array($file->get_id()); 588 $this->generator->create_record($record); 589 $this->generator->create_record($record); 590 $this->search->index(); 591 592 $querydata = new stdClass(); 593 594 // Then search to make sure they are there. 595 $querydata->q = '"File contents"'; 596 $results = $this->search->search($querydata); 597 $this->assertCount(2, $results); 598 599 $first = reset($results); 600 $deleteid = $first->get('id'); 601 602 $this->engine->delete_by_id($deleteid); 603 604 // Check that we don't get a result for it anymore. 605 $results = $this->search->search($querydata); 606 $this->assertCount(1, $results); 607 $result = reset($results); 608 $this->assertNotEquals($deleteid, $result->get('id')); 609 } 610 611 /** 612 * Test that expected results are returned, even with low check_access success rate. 613 * 614 * @dataProvider file_indexing_provider 615 */ 616 public function test_solr_filling($fileindexing) { 617 $this->engine->test_set_config('fileindexing', $fileindexing); 618 619 $user1 = self::getDataGenerator()->create_user(); 620 $user2 = self::getDataGenerator()->create_user(); 621 622 // We are going to create a bunch of records that user 1 can see with 2 keywords. 623 // Then we are going to create a bunch for user 2 with only 1 of the keywords. 624 // If user 2 searches for both keywords, solr will return all of the user 1 results, then the user 2 results. 625 // This is because the user 1 results will match 2 keywords, while the others will match only 1. 626 627 $record = new \stdClass(); 628 629 // First create a bunch of records for user 1 to see. 630 $record->denyuserids = array($user2->id); 631 $record->content = 'Something1 Something2'; 632 $maxresults = (int)(\core_search\manager::MAX_RESULTS * .75); 633 for ($i = 0; $i < $maxresults; $i++) { 634 $this->generator->create_record($record); 635 } 636 637 // Then create a bunch of records for user 2 to see. 638 $record->denyuserids = array($user1->id); 639 $record->content = 'Something1'; 640 for ($i = 0; $i < $maxresults; $i++) { 641 $this->generator->create_record($record); 642 } 643 644 $this->search->index(); 645 646 // Check that user 1 sees all their results. 647 $this->setUser($user1); 648 $querydata = new stdClass(); 649 $querydata->q = 'Something1 Something2'; 650 $results = $this->search->search($querydata); 651 $this->assertCount($maxresults, $results); 652 653 // Check that user 2 will see theirs, even though they may be crouded out. 654 $this->setUser($user2); 655 $results = $this->search->search($querydata); 656 $this->assertCount($maxresults, $results); 657 } 658 659 /** 660 * Create 40 docs, that will be return from Solr in 10 hidden, 10 visible, 10 hidden, 10 visible if you query for: 661 * Something1 Something2 Something3 Something4, with the specified user set. 662 */ 663 protected function setup_user_hidden_docs($user) { 664 // These results will come first, and will not be visible by the user. 665 $record = new \stdClass(); 666 $record->denyuserids = array($user->id); 667 $record->content = 'Something1 Something2 Something3 Something4'; 668 for ($i = 0; $i < 10; $i++) { 669 $this->generator->create_record($record); 670 } 671 672 // These results will come second, and will be visible by the user. 673 unset($record->denyuserids); 674 $record->content = 'Something1 Something2 Something3'; 675 for ($i = 0; $i < 10; $i++) { 676 $this->generator->create_record($record); 677 } 678 679 // These results will come third, and will not be visible by the user. 680 $record->denyuserids = array($user->id); 681 $record->content = 'Something1 Something2'; 682 for ($i = 0; $i < 10; $i++) { 683 $this->generator->create_record($record); 684 } 685 686 // These results will come fourth, and will be visible by the user. 687 unset($record->denyuserids); 688 $record->content = 'Something1 '; 689 for ($i = 0; $i < 10; $i++) { 690 $this->generator->create_record($record); 691 } 692 } 693 694 /** 695 * Test that counts are what we expect. 696 * 697 * @dataProvider file_indexing_provider 698 */ 699 public function test_get_query_total_count($fileindexing) { 700 $this->engine->test_set_config('fileindexing', $fileindexing); 701 702 $user = self::getDataGenerator()->create_user(); 703 $this->setup_user_hidden_docs($user); 704 $this->search->index(); 705 706 $this->setUser($user); 707 $querydata = new stdClass(); 708 $querydata->q = 'Something1 Something2 Something3 Something4'; 709 710 // In this first set, it should have determined the first 10 of 40 are bad, so there could be up to 30 left. 711 $results = $this->engine->execute_query($querydata, (object)['everything' => true], 5); 712 $this->assertEquals(30, $this->engine->get_query_total_count()); 713 $this->assertCount(5, $results); 714 715 // To get to 15, it has to process the first 10 that are bad, 10 that are good, 10 that are bad, then 5 that are good. 716 // So we now know 20 are bad out of 40. 717 $results = $this->engine->execute_query($querydata, (object)['everything' => true], 15); 718 $this->assertEquals(20, $this->engine->get_query_total_count()); 719 $this->assertCount(15, $results); 720 721 // Try to get more then all, make sure we still see 20 count and 20 returned. 722 $results = $this->engine->execute_query($querydata, (object)['everything' => true], 30); 723 $this->assertEquals(20, $this->engine->get_query_total_count()); 724 $this->assertCount(20, $results); 725 } 726 727 /** 728 * Test that paged results are what we expect. 729 * 730 * @dataProvider file_indexing_provider 731 */ 732 public function test_manager_paged_search($fileindexing) { 733 $this->engine->test_set_config('fileindexing', $fileindexing); 734 735 $user = self::getDataGenerator()->create_user(); 736 $this->setup_user_hidden_docs($user); 737 $this->search->index(); 738 739 // Check that user 1 sees all their results. 740 $this->setUser($user); 741 $querydata = new stdClass(); 742 $querydata->q = 'Something1 Something2 Something3 Something4'; 743 744 // On this first page, it should have determined the first 10 of 40 are bad, so there could be up to 30 left. 745 $results = $this->search->paged_search($querydata, 0); 746 $this->assertEquals(30, $results->totalcount); 747 $this->assertCount(10, $results->results); 748 $this->assertEquals(0, $results->actualpage); 749 750 // On the second page, it should have found the next 10 bad ones, so we no know there are only 20 total. 751 $results = $this->search->paged_search($querydata, 1); 752 $this->assertEquals(20, $results->totalcount); 753 $this->assertCount(10, $results->results); 754 $this->assertEquals(1, $results->actualpage); 755 756 // Try to get an additional page - we should get back page 1 results, since that is the last page with valid results. 757 $results = $this->search->paged_search($querydata, 2); 758 $this->assertEquals(20, $results->totalcount); 759 $this->assertCount(10, $results->results); 760 $this->assertEquals(1, $results->actualpage); 761 } 762 763 /** 764 * Tests searching for results restricted to context id. 765 */ 766 public function test_context_restriction() { 767 // Use real search areas. 768 $this->search->clear_static(); 769 $this->search->add_core_search_areas(); 770 771 // Create 2 courses and some forums. 772 $generator = $this->getDataGenerator(); 773 $course1 = $generator->create_course(['fullname' => 'Course 1', 'summary' => 'xyzzy']); 774 $contextc1 = \context_course::instance($course1->id); 775 $course1forum1 = $generator->create_module('forum', ['course' => $course1, 776 'name' => 'C1F1', 'intro' => 'xyzzy']); 777 $contextc1f1 = \context_module::instance($course1forum1->cmid); 778 $course1forum2 = $generator->create_module('forum', ['course' => $course1, 779 'name' => 'C1F2', 'intro' => 'xyzzy']); 780 $contextc1f2 = \context_module::instance($course1forum2->cmid); 781 $course2 = $generator->create_course(['fullname' => 'Course 2', 'summary' => 'xyzzy']); 782 $contextc2 = \context_course::instance($course1->id); 783 $course2forum = $generator->create_module('forum', ['course' => $course2, 784 'name' => 'C2F', 'intro' => 'xyzzy']); 785 $contextc2f = \context_module::instance($course2forum->cmid); 786 787 // Index the courses and forums. 788 $this->search->index(); 789 790 // Search as admin user should find everything. 791 $querydata = new stdClass(); 792 $querydata->q = 'xyzzy'; 793 $results = $this->search->search($querydata); 794 $this->assert_result_titles( 795 ['Course 1', 'Course 2', 'C1F1', 'C1F2', 'C2F'], $results); 796 797 // Admin user manually restricts results by context id to include one course and one forum. 798 $querydata->contextids = [$contextc2f->id, $contextc1->id]; 799 $results = $this->search->search($querydata); 800 $this->assert_result_titles(['Course 1', 'C2F'], $results); 801 802 // Student enrolled in only one course, same restriction, only has the available results. 803 $student2 = $generator->create_user(); 804 $generator->enrol_user($student2->id, $course2->id, 'student'); 805 $this->setUser($student2); 806 $results = $this->search->search($querydata); 807 $this->assert_result_titles(['C2F'], $results); 808 809 // Student enrolled in both courses, same restriction, same results as admin. 810 $student1 = $generator->create_user(); 811 $generator->enrol_user($student1->id, $course1->id, 'student'); 812 $generator->enrol_user($student1->id, $course2->id, 'student'); 813 $this->setUser($student1); 814 $results = $this->search->search($querydata); 815 $this->assert_result_titles(['Course 1', 'C2F'], $results); 816 817 // Restrict both course and context. 818 $querydata->courseids = [$course2->id]; 819 $results = $this->search->search($querydata); 820 $this->assert_result_titles(['C2F'], $results); 821 unset($querydata->courseids); 822 823 // Restrict both area and context. 824 $querydata->areaids = ['core_course-course']; 825 $results = $this->search->search($querydata); 826 $this->assert_result_titles(['Course 1'], $results); 827 828 // Restrict area and context, incompatibly - this has no results (and doesn't do a query). 829 $querydata->contextids = [$contextc2f->id]; 830 $results = $this->search->search($querydata); 831 $this->assert_result_titles([], $results); 832 } 833 834 /** 835 * Tests searching for results in groups, either by specified group ids or based on user 836 * access permissions. 837 */ 838 public function test_groups() { 839 global $USER; 840 841 // Use real search areas. 842 $this->search->clear_static(); 843 $this->search->add_core_search_areas(); 844 845 // Create 2 courses and a selection of forums with different group mode. 846 $generator = $this->getDataGenerator(); 847 $course1 = $generator->create_course(['fullname' => 'Course 1']); 848 $forum1nogroups = $generator->create_module('forum', ['course' => $course1, 'groupmode' => NOGROUPS]); 849 $forum1separategroups = $generator->create_module('forum', ['course' => $course1, 'groupmode' => SEPARATEGROUPS]); 850 $forum1visiblegroups = $generator->create_module('forum', ['course' => $course1, 'groupmode' => VISIBLEGROUPS]); 851 $course2 = $generator->create_course(['fullname' => 'Course 2']); 852 $forum2separategroups = $generator->create_module('forum', ['course' => $course2, 'groupmode' => SEPARATEGROUPS]); 853 854 // Create two groups on each course. 855 $group1a = $generator->create_group(['courseid' => $course1->id]); 856 $group1b = $generator->create_group(['courseid' => $course1->id]); 857 $group2a = $generator->create_group(['courseid' => $course2->id]); 858 $group2b = $generator->create_group(['courseid' => $course2->id]); 859 860 // Create search records in each activity and (where relevant) in each group. 861 $forumgenerator = $generator->get_plugin_generator('mod_forum'); 862 $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id, 863 'forum' => $forum1nogroups->id, 'name' => 'F1NG', 'message' => 'xyzzy']); 864 $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id, 865 'forum' => $forum1separategroups->id, 'name' => 'F1SG-A', 'message' => 'xyzzy', 866 'groupid' => $group1a->id]); 867 $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id, 868 'forum' => $forum1separategroups->id, 'name' => 'F1SG-B', 'message' => 'xyzzy', 869 'groupid' => $group1b->id]); 870 $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id, 871 'forum' => $forum1visiblegroups->id, 'name' => 'F1VG-A', 'message' => 'xyzzy', 872 'groupid' => $group1a->id]); 873 $forumgenerator->create_discussion(['course' => $course1->id, 'userid' => $USER->id, 874 'forum' => $forum1visiblegroups->id, 'name' => 'F1VG-B', 'message' => 'xyzzy', 875 'groupid' => $group1b->id]); 876 $forumgenerator->create_discussion(['course' => $course2->id, 'userid' => $USER->id, 877 'forum' => $forum2separategroups->id, 'name' => 'F2SG-A', 'message' => 'xyzzy', 878 'groupid' => $group2a->id]); 879 $forumgenerator->create_discussion(['course' => $course2->id, 'userid' => $USER->id, 880 'forum' => $forum2separategroups->id, 'name' => 'F2SG-B', 'message' => 'xyzzy', 881 'groupid' => $group2b->id]); 882 883 $this->search->index(); 884 885 // Search as admin user should find everything. 886 $querydata = new stdClass(); 887 $querydata->q = 'xyzzy'; 888 $results = $this->search->search($querydata); 889 $this->assert_result_titles( 890 ['F1NG', 'F1SG-A', 'F1SG-B', 'F1VG-A', 'F1VG-B', 'F2SG-A', 'F2SG-B'], $results); 891 892 // Admin user manually restricts results by groups. 893 $querydata->groupids = [$group1b->id, $group2a->id]; 894 $results = $this->search->search($querydata); 895 $this->assert_result_titles(['F1SG-B', 'F1VG-B', 'F2SG-A'], $results); 896 897 // Student enrolled in both courses but no groups. 898 $student1 = $generator->create_user(); 899 $generator->enrol_user($student1->id, $course1->id, 'student'); 900 $generator->enrol_user($student1->id, $course2->id, 'student'); 901 $this->setUser($student1); 902 903 unset($querydata->groupids); 904 $results = $this->search->search($querydata); 905 $this->assert_result_titles(['F1NG', 'F1VG-A', 'F1VG-B'], $results); 906 907 // Student enrolled in both courses and group A in both cases. 908 $student2 = $generator->create_user(); 909 $generator->enrol_user($student2->id, $course1->id, 'student'); 910 $generator->enrol_user($student2->id, $course2->id, 'student'); 911 groups_add_member($group1a, $student2); 912 groups_add_member($group2a, $student2); 913 $this->setUser($student2); 914 915 $results = $this->search->search($querydata); 916 $this->assert_result_titles(['F1NG', 'F1SG-A', 'F1VG-A', 'F1VG-B', 'F2SG-A'], $results); 917 918 // Manually restrict results to group B in course 1. 919 $querydata->groupids = [$group1b->id]; 920 $results = $this->search->search($querydata); 921 $this->assert_result_titles(['F1VG-B'], $results); 922 923 // Manually restrict results to group A in course 1. 924 $querydata->groupids = [$group1a->id]; 925 $results = $this->search->search($querydata); 926 $this->assert_result_titles(['F1SG-A', 'F1VG-A'], $results); 927 928 // Manager enrolled in both courses (has access all groups). 929 $manager = $generator->create_user(); 930 $generator->enrol_user($manager->id, $course1->id, 'manager'); 931 $generator->enrol_user($manager->id, $course2->id, 'manager'); 932 $this->setUser($manager); 933 unset($querydata->groupids); 934 $results = $this->search->search($querydata); 935 $this->assert_result_titles( 936 ['F1NG', 'F1SG-A', 'F1SG-B', 'F1VG-A', 'F1VG-B', 'F2SG-A', 'F2SG-B'], $results); 937 } 938 939 /** 940 * Tests searching for results restricted to specific user id(s). 941 */ 942 public function test_user_restriction() { 943 // Use real search areas. 944 $this->search->clear_static(); 945 $this->search->add_core_search_areas(); 946 947 // Create a course, a forum, and a glossary. 948 $generator = $this->getDataGenerator(); 949 $course = $generator->create_course(); 950 $forum = $generator->create_module('forum', ['course' => $course->id]); 951 $glossary = $generator->create_module('glossary', ['course' => $course->id]); 952 953 // Create 3 user accounts, all enrolled as students on the course. 954 $user1 = $generator->create_user(); 955 $user2 = $generator->create_user(); 956 $user3 = $generator->create_user(); 957 $generator->enrol_user($user1->id, $course->id, 'student'); 958 $generator->enrol_user($user2->id, $course->id, 'student'); 959 $generator->enrol_user($user3->id, $course->id, 'student'); 960 961 // All users create a forum discussion. 962 $forumgen = $generator->get_plugin_generator('mod_forum'); 963 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id, 964 'userid' => $user1->id, 'name' => 'Post1', 'message' => 'plugh']); 965 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id, 966 'userid' => $user2->id, 'name' => 'Post2', 'message' => 'plugh']); 967 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id, 968 'userid' => $user3->id, 'name' => 'Post3', 'message' => 'plugh']); 969 970 // Two of the users create entries in the glossary. 971 $glossarygen = $generator->get_plugin_generator('mod_glossary'); 972 $glossarygen->create_content($glossary, ['concept' => 'Entry1', 'definition' => 'plugh', 973 'userid' => $user1->id]); 974 $glossarygen->create_content($glossary, ['concept' => 'Entry3', 'definition' => 'plugh', 975 'userid' => $user3->id]); 976 977 // Index the data. 978 $this->search->index(); 979 980 // Search without user restriction should find everything. 981 $querydata = new stdClass(); 982 $querydata->q = 'plugh'; 983 $results = $this->search->search($querydata); 984 $this->assert_result_titles( 985 ['Entry1', 'Entry3', 'Post1', 'Post2', 'Post3'], $results); 986 987 // Restriction to user 3 only. 988 $querydata->userids = [$user3->id]; 989 $results = $this->search->search($querydata); 990 $this->assert_result_titles( 991 ['Entry3', 'Post3'], $results); 992 993 // Restriction to users 1 and 2. 994 $querydata->userids = [$user1->id, $user2->id]; 995 $results = $this->search->search($querydata); 996 $this->assert_result_titles( 997 ['Entry1', 'Post1', 'Post2'], $results); 998 999 // Restriction to users 1 and 2 combined with context restriction. 1000 $querydata->contextids = [context_module::instance($glossary->cmid)->id]; 1001 $results = $this->search->search($querydata); 1002 $this->assert_result_titles( 1003 ['Entry1'], $results); 1004 1005 // Restriction to users 1 and 2 combined with area restriction. 1006 unset($querydata->contextids); 1007 $querydata->areaids = [\core_search\manager::generate_areaid('mod_forum', 'post')]; 1008 $results = $this->search->search($querydata); 1009 $this->assert_result_titles( 1010 ['Post1', 'Post2'], $results); 1011 } 1012 1013 /** 1014 * Tests searching for results containing words in italic text. (This used to fail.) 1015 */ 1016 public function test_italics() { 1017 global $USER; 1018 1019 // Use real search areas. 1020 $this->search->clear_static(); 1021 $this->search->add_core_search_areas(); 1022 1023 // Create a course and a forum. 1024 $generator = $this->getDataGenerator(); 1025 $course = $generator->create_course(); 1026 $forum = $generator->create_module('forum', ['course' => $course->id]); 1027 1028 // As admin user, create forum discussions with various words in italics or with underlines. 1029 $this->setAdminUser(); 1030 $forumgen = $generator->get_plugin_generator('mod_forum'); 1031 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id, 1032 'userid' => $USER->id, 'name' => 'Post1', 1033 'message' => '<p>This is a post about <i>frogs</i>.</p>']); 1034 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id, 1035 'userid' => $USER->id, 'name' => 'Post2', 1036 'message' => '<p>This is a post about <i>toads and zombies</i>.</p>']); 1037 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id, 1038 'userid' => $USER->id, 'name' => 'Post3', 1039 'message' => '<p>This is a post about toads_and_zombies.</p>']); 1040 $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id, 1041 'userid' => $USER->id, 'name' => 'Post4', 1042 'message' => '<p>This is a post about _leading and trailing_ underlines.</p>']); 1043 1044 // Index the data. 1045 $this->search->index(); 1046 1047 // Search for 'frogs' should find the post. 1048 $querydata = new stdClass(); 1049 $querydata->q = 'frogs'; 1050 $results = $this->search->search($querydata); 1051 $this->assert_result_titles(['Post1'], $results); 1052 1053 // Search for 'toads' or 'zombies' should find post 2 (and not 3)... 1054 $querydata->q = 'toads'; 1055 $results = $this->search->search($querydata); 1056 $this->assert_result_titles(['Post2'], $results); 1057 $querydata->q = 'zombies'; 1058 $results = $this->search->search($querydata); 1059 $this->assert_result_titles(['Post2'], $results); 1060 1061 // Search for 'toads_and_zombies' should find post 3. 1062 $querydata->q = 'toads_and_zombies'; 1063 $results = $this->search->search($querydata); 1064 $this->assert_result_titles(['Post3'], $results); 1065 1066 // Search for '_leading' or 'trailing_' should find post 4. 1067 $querydata->q = '_leading'; 1068 $results = $this->search->search($querydata); 1069 $this->assert_result_titles(['Post4'], $results); 1070 $querydata->q = 'trailing_'; 1071 $results = $this->search->search($querydata); 1072 $this->assert_result_titles(['Post4'], $results); 1073 } 1074 1075 /** 1076 * Asserts that the returned documents have the expected titles (regardless of order). 1077 * 1078 * @param string[] $expected List of expected document titles 1079 * @param \core_search\document[] $results List of returned documents 1080 */ 1081 protected function assert_result_titles(array $expected, array $results) { 1082 $titles = []; 1083 foreach ($results as $result) { 1084 $titles[] = $result->get('title'); 1085 } 1086 sort($titles); 1087 sort($expected); 1088 $this->assertEquals($expected, $titles); 1089 } 1090 1091 /** 1092 * Tests the get_supported_orders function for contexts where we can only use relevance 1093 * (system, category). 1094 */ 1095 public function test_get_supported_orders_relevance_only() { 1096 global $DB; 1097 1098 // System or category context: relevance only. 1099 $orders = $this->engine->get_supported_orders(\context_system::instance()); 1100 $this->assertCount(1, $orders); 1101 $this->assertArrayHasKey('relevance', $orders); 1102 1103 $categoryid = $DB->get_field_sql('SELECT MIN(id) FROM {course_categories}'); 1104 $orders = $this->engine->get_supported_orders(\context_coursecat::instance($categoryid)); 1105 $this->assertCount(1, $orders); 1106 $this->assertArrayHasKey('relevance', $orders); 1107 } 1108 1109 /** 1110 * Tests the get_supported_orders function for contexts where we support location as well 1111 * (course, activity, block). 1112 */ 1113 public function test_get_supported_orders_relevance_and_location() { 1114 global $DB; 1115 1116 // Test with course context. 1117 $generator = $this->getDataGenerator(); 1118 $course = $generator->create_course(['fullname' => 'Frogs']); 1119 $coursecontext = \context_course::instance($course->id); 1120 1121 $orders = $this->engine->get_supported_orders($coursecontext); 1122 $this->assertCount(2, $orders); 1123 $this->assertArrayHasKey('relevance', $orders); 1124 $this->assertArrayHasKey('location', $orders); 1125 $this->assertContains('Course: Frogs', $orders['location']); 1126 1127 // Test with activity context. 1128 $page = $generator->create_module('page', ['course' => $course->id, 'name' => 'Toads']); 1129 1130 $orders = $this->engine->get_supported_orders(\context_module::instance($page->cmid)); 1131 $this->assertCount(2, $orders); 1132 $this->assertArrayHasKey('relevance', $orders); 1133 $this->assertArrayHasKey('location', $orders); 1134 $this->assertContains('Page: Toads', $orders['location']); 1135 1136 // Test with block context. 1137 $instance = (object)['blockname' => 'html', 'parentcontextid' => $coursecontext->id, 1138 'showinsubcontexts' => 0, 'pagetypepattern' => 'course-view-*', 1139 'defaultweight' => 0, 'timecreated' => 1, 'timemodified' => 1, 1140 'configdata' => '']; 1141 $blockid = $DB->insert_record('block_instances', $instance); 1142 $blockcontext = \context_block::instance($blockid); 1143 1144 $orders = $this->engine->get_supported_orders($blockcontext); 1145 $this->assertCount(2, $orders); 1146 $this->assertArrayHasKey('relevance', $orders); 1147 $this->assertArrayHasKey('location', $orders); 1148 $this->assertContains('Block: HTML', $orders['location']); 1149 } 1150 1151 /** 1152 * Tests ordering by relevance vs location. 1153 */ 1154 public function test_ordering() { 1155 // Create 2 courses and 2 activities. 1156 $generator = $this->getDataGenerator(); 1157 $course1 = $generator->create_course(['fullname' => 'Course 1']); 1158 $course1context = \context_course::instance($course1->id); 1159 $course1page = $generator->create_module('page', ['course' => $course1]); 1160 $course1pagecontext = \context_module::instance($course1page->cmid); 1161 $course2 = $generator->create_course(['fullname' => 'Course 2']); 1162 $course2context = \context_course::instance($course2->id); 1163 $course2page = $generator->create_module('page', ['course' => $course2]); 1164 $course2pagecontext = \context_module::instance($course2page->cmid); 1165 1166 // Create one search record in each activity and course. 1167 $this->create_search_record($course1->id, $course1context->id, 'C1', 'Xyzzy'); 1168 $this->create_search_record($course1->id, $course1pagecontext->id, 'C1P', 'Xyzzy'); 1169 $this->create_search_record($course2->id, $course2context->id, 'C2', 'Xyzzy'); 1170 $this->create_search_record($course2->id, $course2pagecontext->id, 'C2P', 'Xyzzy plugh'); 1171 $this->search->index(); 1172 1173 // Default search works by relevance so the one with both words should be top. 1174 $querydata = new stdClass(); 1175 $querydata->q = 'xyzzy plugh'; 1176 $results = $this->search->search($querydata); 1177 $this->assertCount(4, $results); 1178 $this->assertEquals('C2P', $results[0]->get('title')); 1179 1180 // Same if you explicitly specify relevance. 1181 $querydata->order = 'relevance'; 1182 $results = $this->search->search($querydata); 1183 $this->assertEquals('C2P', $results[0]->get('title')); 1184 1185 // If you specify order by location and you are in C2 or C2P then results are the same. 1186 $querydata->order = 'location'; 1187 $querydata->context = $course2context; 1188 $results = $this->search->search($querydata); 1189 $this->assertEquals('C2P', $results[0]->get('title')); 1190 $querydata->context = $course2pagecontext; 1191 $results = $this->search->search($querydata); 1192 $this->assertEquals('C2P', $results[0]->get('title')); 1193 1194 // But if you are in C1P then you get different results (C1P first). 1195 $querydata->context = $course1pagecontext; 1196 $results = $this->search->search($querydata); 1197 $this->assertEquals('C1P', $results[0]->get('title')); 1198 } 1199 1200 /** 1201 * Tests with bogus content (that can be entered into Moodle) to see if it crashes. 1202 */ 1203 public function test_bogus_content() { 1204 $generator = $this->getDataGenerator(); 1205 $course1 = $generator->create_course(['fullname' => 'Course 1']); 1206 $course1context = \context_course::instance($course1->id); 1207 1208 // It is possible to enter into a Moodle database content containing these characters, 1209 // which are Unicode non-characters / byte order marks. If sent to Solr, these cause 1210 // failures. 1211 $boguscontent = html_entity_decode('') . 'frog'; 1212 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent); 1213 $boguscontent = html_entity_decode('') . 'frog'; 1214 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent); 1215 1216 // Unicode Standard Version 9.0 - Core Specification, section 23.7, lists 66 non-characters 1217 // in total. Here are some of them - these work OK for me but it may depend on platform. 1218 $boguscontent = html_entity_decode('') . 'frog'; 1219 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent); 1220 $boguscontent = html_entity_decode('') . 'frog'; 1221 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent); 1222 $boguscontent = html_entity_decode('') . 'frog'; 1223 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent); 1224 $boguscontent = html_entity_decode('') . 'frog'; 1225 $this->create_search_record($course1->id, $course1context->id, 'C1', $boguscontent); 1226 1227 // Do the indexing (this will check it doesn't throw warnings). 1228 $this->search->index(); 1229 1230 // Confirm that all 6 documents are found in search. 1231 $querydata = new stdClass(); 1232 $querydata->q = 'frog'; 1233 $results = $this->search->search($querydata); 1234 $this->assertCount(6, $results); 1235 } 1236 1237 /** 1238 * Adds a record to the mock search area, so that the search engine can find it later. 1239 * 1240 * @param int $courseid Course id 1241 * @param int $contextid Context id 1242 * @param string $title Title for search index 1243 * @param string $content Content for search index 1244 */ 1245 protected function create_search_record($courseid, $contextid, $title, $content) { 1246 $record = new \stdClass(); 1247 $record->content = $content; 1248 $record->title = $title; 1249 $record->courseid = $courseid; 1250 $record->contextid = $contextid; 1251 $this->generator->create_record($record); 1252 } 1253 1254 /** 1255 * Tries out deleting data for a context or a course. 1256 * 1257 * @throws coding_exception 1258 * @throws moodle_exception 1259 */ 1260 public function test_deleted_contexts_and_courses() { 1261 // Create some courses and activities. 1262 $generator = $this->getDataGenerator(); 1263 $course1 = $generator->create_course(['fullname' => 'Course 1']); 1264 $course1context = \context_course::instance($course1->id); 1265 $course1page1 = $generator->create_module('page', ['course' => $course1]); 1266 $course1page1context = \context_module::instance($course1page1->cmid); 1267 $course1page2 = $generator->create_module('page', ['course' => $course1]); 1268 $course1page2context = \context_module::instance($course1page2->cmid); 1269 $course2 = $generator->create_course(['fullname' => 'Course 2']); 1270 $course2context = \context_course::instance($course2->id); 1271 $course2page = $generator->create_module('page', ['course' => $course2]); 1272 $course2pagecontext = \context_module::instance($course2page->cmid); 1273 1274 // Create one search record in each activity and course. 1275 $this->create_search_record($course1->id, $course1context->id, 'C1', 'Xyzzy'); 1276 $this->create_search_record($course1->id, $course1page1context->id, 'C1P1', 'Xyzzy'); 1277 $this->create_search_record($course1->id, $course1page2context->id, 'C1P2', 'Xyzzy'); 1278 $this->create_search_record($course2->id, $course2context->id, 'C2', 'Xyzzy'); 1279 $this->create_search_record($course2->id, $course2pagecontext->id, 'C2P', 'Xyzzy plugh'); 1280 $this->search->index(); 1281 1282 // By default we have all results. 1283 $this->assert_raw_solr_query_result('content:xyzzy', ['C1', 'C1P1', 'C1P2', 'C2', 'C2P']); 1284 1285 // Say we delete the course2pagecontext... 1286 $this->engine->delete_index_for_context($course2pagecontext->id); 1287 $this->assert_raw_solr_query_result('content:xyzzy', ['C1', 'C1P1', 'C1P2', 'C2']); 1288 1289 // Now delete the second course... 1290 $this->engine->delete_index_for_course($course2->id); 1291 $this->assert_raw_solr_query_result('content:xyzzy', ['C1', 'C1P1', 'C1P2']); 1292 1293 // Finally let's delete using Moodle functions to check that works. Single context first. 1294 course_delete_module($course1page1->cmid); 1295 $this->assert_raw_solr_query_result('content:xyzzy', ['C1', 'C1P2']); 1296 delete_course($course1, false); 1297 $this->assert_raw_solr_query_result('content:xyzzy', []); 1298 } 1299 1300 /** 1301 * Carries out a raw Solr query using the Solr basic query syntax. 1302 * 1303 * This is used to test data contained in the index without going through Moodle processing. 1304 * 1305 * @param string $q Search query 1306 * @param string[] $expected Expected titles of results, in alphabetical order 1307 */ 1308 protected function assert_raw_solr_query_result(string $q, array $expected) { 1309 $solr = $this->engine->get_search_client_public(); 1310 $query = new SolrQuery($q); 1311 $results = $solr->query($query)->getResponse()->response->docs; 1312 if ($results) { 1313 $titles = array_map(function($x) { 1314 return $x->title; 1315 }, $results); 1316 sort($titles); 1317 } else { 1318 $titles = []; 1319 } 1320 $this->assertEquals($expected, $titles); 1321 } 1322 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body