Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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 core;
  18  
  19  use advanced_testcase;
  20  use cache;
  21  use cm_info;
  22  use coding_exception;
  23  use context_course;
  24  use context_module;
  25  use course_modinfo;
  26  use moodle_exception;
  27  use moodle_url;
  28  use Exception;
  29  
  30  /**
  31   * Unit tests for lib/modinfolib.php.
  32   *
  33   * @package    core
  34   * @category   phpunit
  35   * @copyright  2012 Andrew Davis
  36   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class modinfolib_test extends advanced_testcase {
  39      public function test_section_info_properties() {
  40          global $DB, $CFG;
  41  
  42          $this->resetAfterTest();
  43          $oldcfgenableavailability = $CFG->enableavailability;
  44          $oldcfgenablecompletion = $CFG->enablecompletion;
  45          set_config('enableavailability', 1);
  46          set_config('enablecompletion', 1);
  47          $this->setAdminUser();
  48  
  49          // Generate the course and pre-requisite module.
  50          $course = $this->getDataGenerator()->create_course(
  51                  array('format' => 'topics',
  52                      'numsections' => 3,
  53                      'enablecompletion' => 1,
  54                      'groupmode' => SEPARATEGROUPS,
  55                      'forcegroupmode' => 0),
  56                  array('createsections' => true));
  57          $coursecontext = context_course::instance($course->id);
  58          $prereqforum = $this->getDataGenerator()->create_module('forum',
  59                  array('course' => $course->id),
  60                  array('completion' => 1));
  61  
  62          // Add availability conditions.
  63          $availability = '{"op":"&","showc":[true,true,true],"c":[' .
  64                  '{"type":"completion","cm":' . $prereqforum->cmid . ',"e":"' .
  65                      COMPLETION_COMPLETE . '"},' .
  66                  '{"type":"grade","id":666,"min":0.4},' .
  67                  '{"type":"profile","op":"contains","sf":"email","v":"test"}' .
  68                  ']}';
  69          $DB->set_field('course_sections', 'availability', $availability,
  70                  array('course' => $course->id, 'section' => 2));
  71          rebuild_course_cache($course->id, true);
  72          $sectiondb = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 2));
  73  
  74          // Create and enrol a student.
  75          $studentrole = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
  76          $student = $this->getDataGenerator()->create_user();
  77          role_assign($studentrole->id, $student->id, $coursecontext);
  78          $enrolplugin = enrol_get_plugin('manual');
  79          $enrolinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual'));
  80          $enrolplugin->enrol_user($enrolinstance, $student->id);
  81          $this->setUser($student);
  82  
  83          // Get modinfo.
  84          $modinfo = get_fast_modinfo($course->id);
  85          $si = $modinfo->get_section_info(2);
  86  
  87          $this->assertEquals($sectiondb->id, $si->id);
  88          $this->assertEquals($sectiondb->course, $si->course);
  89          $this->assertEquals($sectiondb->section, $si->section);
  90          $this->assertEquals($sectiondb->name, $si->name);
  91          $this->assertEquals($sectiondb->visible, $si->visible);
  92          $this->assertEquals($sectiondb->summary, $si->summary);
  93          $this->assertEquals($sectiondb->summaryformat, $si->summaryformat);
  94          $this->assertEquals($sectiondb->sequence, $si->sequence); // Since this section does not contain invalid modules.
  95          $this->assertEquals($availability, $si->availability);
  96  
  97          // Dynamic fields, just test that they can be retrieved (must be carefully tested in each activity type).
  98          $this->assertEquals(0, $si->available);
  99          $this->assertNotEmpty($si->availableinfo); // Lists all unmet availability conditions.
 100          $this->assertEquals(0, $si->uservisible);
 101  
 102          // Restore settings.
 103          set_config('enableavailability', $oldcfgenableavailability);
 104          set_config('enablecompletion', $oldcfgenablecompletion);
 105      }
 106  
 107      public function test_cm_info_properties() {
 108          global $DB, $CFG;
 109  
 110          $this->resetAfterTest();
 111          $oldcfgenableavailability = $CFG->enableavailability;
 112          $oldcfgenablecompletion = $CFG->enablecompletion;
 113          set_config('enableavailability', 1);
 114          set_config('enablecompletion', 1);
 115          $this->setAdminUser();
 116  
 117          // Generate the course and pre-requisite module.
 118          $course = $this->getDataGenerator()->create_course(
 119                  array('format' => 'topics',
 120                      'numsections' => 3,
 121                      'enablecompletion' => 1,
 122                      'groupmode' => SEPARATEGROUPS,
 123                      'forcegroupmode' => 0),
 124                  array('createsections' => true));
 125          $coursecontext = context_course::instance($course->id);
 126          $prereqforum = $this->getDataGenerator()->create_module('forum',
 127                  array('course' => $course->id),
 128                  array('completion' => 1));
 129  
 130          // Generate module and add availability conditions.
 131          $availability = '{"op":"&","showc":[true,true,true],"c":[' .
 132                  '{"type":"completion","cm":' . $prereqforum->cmid . ',"e":"' .
 133                      COMPLETION_COMPLETE . '"},' .
 134                  '{"type":"grade","id":666,"min":0.4},' .
 135                  '{"type":"profile","op":"contains","sf":"email","v":"test"}' .
 136                  ']}';
 137          $assign = $this->getDataGenerator()->create_module('assign',
 138                  array('course' => $course->id),
 139                  array('idnumber' => 123,
 140                      'groupmode' => VISIBLEGROUPS,
 141                      'availability' => $availability));
 142          rebuild_course_cache($course->id, true);
 143  
 144          // Retrieve all related records from DB.
 145          $assigndb = $DB->get_record('assign', array('id' => $assign->id));
 146          $moduletypedb = $DB->get_record('modules', array('name' => 'assign'));
 147          $moduledb = $DB->get_record('course_modules', array('module' => $moduletypedb->id, 'instance' => $assign->id));
 148          $sectiondb = $DB->get_record('course_sections', array('id' => $moduledb->section));
 149          $modnamessingular = get_module_types_names(false);
 150          $modnamesplural = get_module_types_names(true);
 151  
 152          // Create and enrol a student.
 153          $studentrole = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
 154          $student = $this->getDataGenerator()->create_user();
 155          role_assign($studentrole->id, $student->id, $coursecontext);
 156          $enrolplugin = enrol_get_plugin('manual');
 157          $enrolinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual'));
 158          $enrolplugin->enrol_user($enrolinstance, $student->id);
 159          $this->setUser($student);
 160  
 161          // Emulate data used in building course cache to receive the same instance of cached_cm_info as was used in building modinfo.
 162          $rawmods = get_course_mods($course->id);
 163          $cachedcminfo = assign_get_coursemodule_info($rawmods[$moduledb->id]);
 164  
 165          // Get modinfo.
 166          $modinfo = get_fast_modinfo($course->id);
 167          $cm = $modinfo->instances['assign'][$assign->id];
 168  
 169          $this->assertEquals($moduledb->id, $cm->id);
 170          $this->assertEquals($assigndb->id, $cm->instance);
 171          $this->assertEquals($moduledb->course, $cm->course);
 172          $this->assertEquals($moduledb->idnumber, $cm->idnumber);
 173          $this->assertEquals($moduledb->added, $cm->added);
 174          $this->assertEquals($moduledb->visible, $cm->visible);
 175          $this->assertEquals($moduledb->visibleold, $cm->visibleold);
 176          $this->assertEquals($moduledb->groupmode, $cm->groupmode);
 177          $this->assertEquals(VISIBLEGROUPS, $cm->groupmode);
 178          $this->assertEquals($moduledb->groupingid, $cm->groupingid);
 179          $this->assertEquals($course->groupmodeforce, $cm->coursegroupmodeforce);
 180          $this->assertEquals($course->groupmode, $cm->coursegroupmode);
 181          $this->assertEquals(SEPARATEGROUPS, $cm->coursegroupmode);
 182          $this->assertEquals($course->groupmodeforce ? $course->groupmode : $moduledb->groupmode,
 183                  $cm->effectivegroupmode); // (since mod_assign supports groups).
 184          $this->assertEquals(VISIBLEGROUPS, $cm->effectivegroupmode);
 185          $this->assertEquals($moduledb->indent, $cm->indent);
 186          $this->assertEquals($moduledb->completion, $cm->completion);
 187          $this->assertEquals($moduledb->completiongradeitemnumber, $cm->completiongradeitemnumber);
 188          $this->assertEquals($moduledb->completionpassgrade, $cm->completionpassgrade);
 189          $this->assertEquals($moduledb->completionview, $cm->completionview);
 190          $this->assertEquals($moduledb->completionexpected, $cm->completionexpected);
 191          $this->assertEquals($moduledb->showdescription, $cm->showdescription);
 192          $this->assertEquals(null, $cm->extra); // Deprecated field. Used in module types that don't return cached_cm_info.
 193          $this->assertEquals($cachedcminfo->icon, $cm->icon);
 194          $this->assertEquals($cachedcminfo->iconcomponent, $cm->iconcomponent);
 195          $this->assertEquals('assign', $cm->modname);
 196          $this->assertEquals($moduledb->module, $cm->module);
 197          $this->assertEquals($cachedcminfo->name, $cm->name);
 198          $this->assertEquals($sectiondb->section, $cm->sectionnum);
 199          $this->assertEquals($moduledb->section, $cm->section);
 200          $this->assertEquals($availability, $cm->availability);
 201          $this->assertEquals(context_module::instance($moduledb->id), $cm->context);
 202          $this->assertEquals($modnamessingular['assign'], $cm->modfullname);
 203          $this->assertEquals($modnamesplural['assign'], $cm->modplural);
 204          $this->assertEquals(new moodle_url('/mod/assign/view.php', array('id' => $moduledb->id)), $cm->url);
 205          $this->assertEquals($cachedcminfo->customdata, $cm->customdata);
 206  
 207          // Dynamic fields, just test that they can be retrieved (must be carefully tested in each activity type).
 208          $this->assertNotEmpty($cm->availableinfo); // Lists all unmet availability conditions.
 209          $this->assertEquals(0, $cm->uservisible);
 210          $this->assertEquals('', $cm->extraclasses);
 211          $this->assertEquals('', $cm->onclick);
 212          $this->assertEquals(null, $cm->afterlink);
 213          $this->assertEquals(null, $cm->afterediticons);
 214          $this->assertEquals('', $cm->content);
 215  
 216          // Attempt to access and set non-existing field.
 217          $this->assertTrue(empty($modinfo->somefield));
 218          $this->assertFalse(isset($modinfo->somefield));
 219          $cm->somefield;
 220          $this->assertDebuggingCalled();
 221          $cm->somefield = 'Some value';
 222          $this->assertDebuggingCalled();
 223          $this->assertEmpty($cm->somefield);
 224          $this->assertDebuggingCalled();
 225  
 226          // Attempt to overwrite an existing field.
 227          $prevvalue = $cm->name;
 228          $this->assertNotEmpty($cm->name);
 229          $this->assertFalse(empty($cm->name));
 230          $this->assertTrue(isset($cm->name));
 231          $cm->name = 'Illegal overwriting';
 232          $this->assertDebuggingCalled();
 233          $this->assertEquals($prevvalue, $cm->name);
 234          $this->assertDebuggingNotCalled();
 235  
 236          // Restore settings.
 237          set_config('enableavailability', $oldcfgenableavailability);
 238          set_config('enablecompletion', $oldcfgenablecompletion);
 239      }
 240  
 241      public function test_matching_cacherev() {
 242          global $DB, $CFG;
 243  
 244          $this->resetAfterTest();
 245          $this->setAdminUser();
 246          $cache = cache::make('core', 'coursemodinfo');
 247  
 248          // Generate the course and pre-requisite module.
 249          $course = $this->getDataGenerator()->create_course(
 250                  array('format' => 'topics',
 251                      'numsections' => 3),
 252                  array('createsections' => true));
 253  
 254          // Make sure the cacherev is set.
 255          $cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
 256          $this->assertGreaterThan(0, $cacherev);
 257          $prevcacherev = $cacherev;
 258  
 259          // Reset course cache and make sure cacherev is bumped up but cache is empty.
 260          rebuild_course_cache($course->id, true);
 261          $cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
 262          $this->assertGreaterThan($prevcacherev, $cacherev);
 263          $this->assertEmpty($cache->get_versioned($course->id, $prevcacherev));
 264          $prevcacherev = $cacherev;
 265  
 266          // Build course cache. Cacherev should not change but cache is now not empty. Make sure cacherev is the same everywhere.
 267          $modinfo = get_fast_modinfo($course->id);
 268          $cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
 269          $this->assertEquals($prevcacherev, $cacherev);
 270          $cachedvalue = $cache->get_versioned($course->id, $cacherev);
 271          $this->assertNotEmpty($cachedvalue);
 272          $this->assertEquals($cacherev, $cachedvalue->cacherev);
 273          $this->assertEquals($cacherev, $modinfo->get_course()->cacherev);
 274          $prevcacherev = $cacherev;
 275  
 276          // Little trick to check that cache is not rebuilt druing the next step - substitute the value in MUC and later check that it is still there.
 277          $cache->acquire_lock($course->id);
 278          $cache->set_versioned($course->id, $cacherev, (object)array_merge((array)$cachedvalue, array('secretfield' => 1)));
 279          $cache->release_lock($course->id);
 280  
 281          // Clear static cache and call get_fast_modinfo() again (pretend we are in another request). Cache should not be rebuilt.
 282          course_modinfo::clear_instance_cache();
 283          $modinfo = get_fast_modinfo($course->id);
 284          $cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
 285          $this->assertEquals($prevcacherev, $cacherev);
 286          $cachedvalue = $cache->get_versioned($course->id, $cacherev);
 287          $this->assertNotEmpty($cachedvalue);
 288          $this->assertEquals($cacherev, $cachedvalue->cacherev);
 289          $this->assertNotEmpty($cachedvalue->secretfield);
 290          $this->assertEquals($cacherev, $modinfo->get_course()->cacherev);
 291          $prevcacherev = $cacherev;
 292  
 293          // Rebuild course cache. Cacherev must be incremented everywhere.
 294          rebuild_course_cache($course->id);
 295          $cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
 296          $this->assertGreaterThan($prevcacherev, $cacherev);
 297          $cachedvalue = $cache->get_versioned($course->id, $cacherev);
 298          $this->assertNotEmpty($cachedvalue);
 299          $this->assertEquals($cacherev, $cachedvalue->cacherev);
 300          $modinfo = get_fast_modinfo($course->id);
 301          $this->assertEquals($cacherev, $modinfo->get_course()->cacherev);
 302          $prevcacherev = $cacherev;
 303  
 304          // Update cacherev in DB and make sure the cache will be rebuilt on the next call to get_fast_modinfo().
 305          increment_revision_number('course', 'cacherev', 'id = ?', array($course->id));
 306          // We need to clear static cache for course_modinfo instances too.
 307          course_modinfo::clear_instance_cache();
 308          $modinfo = get_fast_modinfo($course->id);
 309          $cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
 310          $this->assertGreaterThan($prevcacherev, $cacherev);
 311          $cachedvalue = $cache->get_versioned($course->id, $cacherev);
 312          $this->assertNotEmpty($cachedvalue);
 313          $this->assertEquals($cacherev, $cachedvalue->cacherev);
 314          $this->assertEquals($cacherev, $modinfo->get_course()->cacherev);
 315          $prevcacherev = $cacherev;
 316  
 317          // Reset cache for all courses and make sure this course cache is reset.
 318          rebuild_course_cache(0, true);
 319          $cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
 320          $this->assertGreaterThan($prevcacherev, $cacherev);
 321          $this->assertEmpty($cache->get_versioned($course->id, $cacherev));
 322          // Rebuild again.
 323          $modinfo = get_fast_modinfo($course->id);
 324          $cachedvalue = $cache->get_versioned($course->id, $cacherev);
 325          $this->assertNotEmpty($cachedvalue);
 326          $this->assertEquals($cacherev, $cachedvalue->cacherev);
 327          $this->assertEquals($cacherev, $modinfo->get_course()->cacherev);
 328          $prevcacherev = $cacherev;
 329  
 330          // Purge all caches and make sure cacherev is increased and data from MUC erased.
 331          purge_all_caches();
 332          $cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
 333          $this->assertGreaterThan($prevcacherev, $cacherev);
 334          $this->assertEmpty($cache->get($course->id));
 335      }
 336  
 337      /**
 338       * The cacherev is updated when we rebuild course cache, but there are scenarios where an
 339       * existing course object with old cacherev might be reused within the same request after
 340       * clearing the cache. In that case, we need to check that the new data is loaded and it
 341       * does not reuse the old cached data with old cacherev.
 342       *
 343       * @covers ::rebuild_course_cache()
 344       */
 345      public function test_cache_clear_wrong_cacherev(): void {
 346          global $DB;
 347  
 348          $this->resetAfterTest();
 349          $originalcourse = $this->getDataGenerator()->create_course();
 350          $course = $DB->get_record('course', ['id' => $originalcourse->id]);
 351          $page = $this->getDataGenerator()->create_module('page',
 352                  ['course' => $course->id, 'name' => 'frog']);
 353          $oldmodinfo = get_fast_modinfo($course);
 354          $this->assertEquals('frog', $oldmodinfo->get_cm($page->cmid)->name);
 355  
 356          // Change page name and rebuild cache.
 357          $DB->set_field('page', 'name', 'Frog', ['id' => $page->id]);
 358          rebuild_course_cache($course->id, true);
 359  
 360          // Get modinfo using original course object which has old cacherev.
 361          $newmodinfo = get_fast_modinfo($course);
 362          $this->assertEquals('Frog', $newmodinfo->get_cm($page->cmid)->name);
 363      }
 364  
 365      /**
 366       * When cacherev is updated for a course, it is supposed to update in the $COURSE and $SITE
 367       * globals automatically. Check this is working.
 368       *
 369       * @covers ::rebuild_course_cache()
 370       */
 371      public function test_cacherev_update_in_globals(): void {
 372          global $DB, $COURSE, $SITE;
 373  
 374          $this->resetAfterTest();
 375  
 376          // Create a course and get modinfo.
 377          $originalcourse = $this->getDataGenerator()->create_course();
 378          $oldmodinfo = get_fast_modinfo($originalcourse->id);
 379  
 380          // Store (two clones of) the course in COURSE and SITE globals.
 381          $COURSE = get_course($originalcourse->id);
 382          $SITE = get_course($originalcourse->id);
 383  
 384          // Note original cacherev.
 385          $originalcacherev = $oldmodinfo->get_course()->cacherev;
 386          $this->assertEquals($COURSE->cacherev, $originalcacherev);
 387          $this->assertEquals($SITE->cacherev, $originalcacherev);
 388  
 389          // Clear the cache and check cacherev updated.
 390          rebuild_course_cache($originalcourse->id, true);
 391  
 392          $newcourse = $DB->get_record('course', ['id' => $originalcourse->id]);
 393          $this->assertGreaterThan($originalcacherev, $newcourse->cacherev);
 394  
 395          // Check that the in-memory $COURSE and $SITE have updated.
 396          $this->assertEquals($newcourse->cacherev, $COURSE->cacherev);
 397          $this->assertEquals($newcourse->cacherev, $SITE->cacherev);
 398      }
 399  
 400      public function test_course_modinfo_properties() {
 401          global $USER, $DB;
 402  
 403          $this->resetAfterTest();
 404          $this->setAdminUser();
 405  
 406          // Generate the course and some modules. Make one section hidden.
 407          $course = $this->getDataGenerator()->create_course(
 408                  array('format' => 'topics',
 409                      'numsections' => 3),
 410                  array('createsections' => true));
 411          $DB->execute('UPDATE {course_sections} SET visible = 0 WHERE course = ? and section = ?',
 412                  array($course->id, 3));
 413          $coursecontext = context_course::instance($course->id);
 414          $forum0 = $this->getDataGenerator()->create_module('forum',
 415                  array('course' => $course->id), array('section' => 0));
 416          $assign0 = $this->getDataGenerator()->create_module('assign',
 417                  array('course' => $course->id), array('section' => 0, 'visible' => 0));
 418          $forum1 = $this->getDataGenerator()->create_module('forum',
 419                  array('course' => $course->id), array('section' => 1));
 420          $assign1 = $this->getDataGenerator()->create_module('assign',
 421                  array('course' => $course->id), array('section' => 1));
 422          $page1 = $this->getDataGenerator()->create_module('page',
 423                  array('course' => $course->id), array('section' => 1));
 424          $page3 = $this->getDataGenerator()->create_module('page',
 425                  array('course' => $course->id), array('section' => 3));
 426  
 427          $modinfo = get_fast_modinfo($course->id);
 428  
 429          $this->assertEquals(array($forum0->cmid, $assign0->cmid, $forum1->cmid, $assign1->cmid, $page1->cmid, $page3->cmid),
 430                  array_keys($modinfo->cms));
 431          $this->assertEquals($course->id, $modinfo->courseid);
 432          $this->assertEquals($USER->id, $modinfo->userid);
 433          $this->assertEquals(array(0 => array($forum0->cmid, $assign0->cmid),
 434              1 => array($forum1->cmid, $assign1->cmid, $page1->cmid), 3 => array($page3->cmid)), $modinfo->sections);
 435          $this->assertEquals(array('forum', 'assign', 'page'), array_keys($modinfo->instances));
 436          $this->assertEquals(array($assign0->id, $assign1->id), array_keys($modinfo->instances['assign']));
 437          $this->assertEquals(array($forum0->id, $forum1->id), array_keys($modinfo->instances['forum']));
 438          $this->assertEquals(array($page1->id, $page3->id), array_keys($modinfo->instances['page']));
 439          $this->assertEquals(groups_get_user_groups($course->id), $modinfo->groups);
 440          $this->assertEquals(array(0 => array($forum0->cmid, $assign0->cmid),
 441              1 => array($forum1->cmid, $assign1->cmid, $page1->cmid),
 442              3 => array($page3->cmid)), $modinfo->get_sections());
 443          $this->assertEquals(array(0, 1, 2, 3), array_keys($modinfo->get_section_info_all()));
 444          $this->assertEquals($forum0->cmid . ',' . $assign0->cmid, $modinfo->get_section_info(0)->sequence);
 445          $this->assertEquals($forum1->cmid . ',' . $assign1->cmid . ',' . $page1->cmid, $modinfo->get_section_info(1)->sequence);
 446          $this->assertEquals('', $modinfo->get_section_info(2)->sequence);
 447          $this->assertEquals($page3->cmid, $modinfo->get_section_info(3)->sequence);
 448          $this->assertEquals($course->id, $modinfo->get_course()->id);
 449          $names = array_keys($modinfo->get_used_module_names());
 450          sort($names);
 451          $this->assertEquals(array('assign', 'forum', 'page'), $names);
 452          $names = array_keys($modinfo->get_used_module_names(true));
 453          sort($names);
 454          $this->assertEquals(array('assign', 'forum', 'page'), $names);
 455          // Admin can see hidden modules/sections.
 456          $this->assertTrue($modinfo->cms[$assign0->cmid]->uservisible);
 457          $this->assertTrue($modinfo->get_section_info(3)->uservisible);
 458  
 459          // Get modinfo for non-current user (without capability to view hidden activities/sections).
 460          $user = $this->getDataGenerator()->create_user();
 461          $modinfo = get_fast_modinfo($course->id, $user->id);
 462          $this->assertEquals($user->id, $modinfo->userid);
 463          $this->assertFalse($modinfo->cms[$assign0->cmid]->uservisible);
 464          $this->assertFalse($modinfo->get_section_info(3)->uservisible);
 465  
 466          // Attempt to access and set non-existing field.
 467          $this->assertTrue(empty($modinfo->somefield));
 468          $this->assertFalse(isset($modinfo->somefield));
 469          $modinfo->somefield;
 470          $this->assertDebuggingCalled();
 471          $modinfo->somefield = 'Some value';
 472          $this->assertDebuggingCalled();
 473          $this->assertEmpty($modinfo->somefield);
 474          $this->assertDebuggingCalled();
 475  
 476          // Attempt to overwrite existing field.
 477          $this->assertFalse(empty($modinfo->cms));
 478          $this->assertTrue(isset($modinfo->cms));
 479          $modinfo->cms = 'Illegal overwriting';
 480          $this->assertDebuggingCalled();
 481          $this->assertNotEquals('Illegal overwriting', $modinfo->cms);
 482      }
 483  
 484      public function test_is_user_access_restricted_by_capability() {
 485          global $DB;
 486  
 487          $this->resetAfterTest();
 488  
 489          // Create a course and a mod_assign instance.
 490          $course = $this->getDataGenerator()->create_course();
 491          $assign = $this->getDataGenerator()->create_module('assign', array('course'=>$course->id));
 492  
 493          // Create and enrol a student.
 494          $coursecontext = context_course::instance($course->id);
 495          $studentrole = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
 496          $student = $this->getDataGenerator()->create_user();
 497          role_assign($studentrole->id, $student->id, $coursecontext);
 498          $enrolplugin = enrol_get_plugin('manual');
 499          $enrolinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual'));
 500          $enrolplugin->enrol_user($enrolinstance, $student->id);
 501          $this->setUser($student);
 502  
 503          // Make sure student can see the module.
 504          $cm = get_fast_modinfo($course->id)->instances['assign'][$assign->id];
 505          $this->assertTrue($cm->uservisible);
 506          $this->assertFalse($cm->is_user_access_restricted_by_capability());
 507  
 508          // Prohibit student to view mod_assign for the course.
 509          role_change_permission($studentrole->id, $coursecontext, 'mod/assign:view', CAP_PROHIBIT);
 510          get_fast_modinfo($course->id, 0, true);
 511          $cm = get_fast_modinfo($course->id)->instances['assign'][$assign->id];
 512          $this->assertFalse($cm->uservisible);
 513          $this->assertTrue($cm->is_user_access_restricted_by_capability());
 514  
 515          // Restore permission to student to view mod_assign for the course.
 516          role_change_permission($studentrole->id, $coursecontext, 'mod/assign:view', CAP_INHERIT);
 517          get_fast_modinfo($course->id, 0, true);
 518          $cm = get_fast_modinfo($course->id)->instances['assign'][$assign->id];
 519          $this->assertTrue($cm->uservisible);
 520          $this->assertFalse($cm->is_user_access_restricted_by_capability());
 521  
 522          // Prohibit student to view mod_assign for the particular module.
 523          role_change_permission($studentrole->id, context_module::instance($cm->id), 'mod/assign:view', CAP_PROHIBIT);
 524          get_fast_modinfo($course->id, 0, true);
 525          $cm = get_fast_modinfo($course->id)->instances['assign'][$assign->id];
 526          $this->assertFalse($cm->uservisible);
 527          $this->assertTrue($cm->is_user_access_restricted_by_capability());
 528  
 529          // Check calling get_fast_modinfo() for different user:
 530          $this->setAdminUser();
 531          $cm = get_fast_modinfo($course->id)->instances['assign'][$assign->id];
 532          $this->assertTrue($cm->uservisible);
 533          $this->assertFalse($cm->is_user_access_restricted_by_capability());
 534          $cm = get_fast_modinfo($course->id, $student->id)->instances['assign'][$assign->id];
 535          $this->assertFalse($cm->uservisible);
 536          $this->assertTrue($cm->is_user_access_restricted_by_capability());
 537      }
 538  
 539      /**
 540       * Tests for function cm_info::get_course_module_record()
 541       */
 542      public function test_cm_info_get_course_module_record() {
 543          global $DB;
 544  
 545          $this->resetAfterTest();
 546          $this->setAdminUser();
 547  
 548          set_config('enableavailability', 1);
 549          set_config('enablecompletion', 1);
 550  
 551          $course = $this->getDataGenerator()->create_course(
 552                  array('format' => 'topics', 'numsections' => 3, 'enablecompletion' => 1),
 553                  array('createsections' => true));
 554          $mods = array();
 555          $mods[0] = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
 556          $mods[1] = $this->getDataGenerator()->create_module('assign',
 557                  array('course' => $course->id,
 558                      'section' => 3,
 559                      'idnumber' => '12345',
 560                      'showdescription' => true
 561                      ));
 562          // Pick a small valid availability value to use.
 563          $availabilityvalue = '{"op":"|","show":true,"c":[{"type":"date","d":">=","t":4}]}';
 564          $mods[2] = $this->getDataGenerator()->create_module('book',
 565                  array('course' => $course->id,
 566                      'indent' => 5,
 567                      'availability' => $availabilityvalue,
 568                      'showdescription' => false,
 569                      'completion' => true,
 570                      'completionview' => true,
 571                      'completionexpected' => time() + 5000,
 572                      ));
 573          $mods[3] = $this->getDataGenerator()->create_module('forum',
 574                  array('course' => $course->id,
 575                      'visible' => 0,
 576                      'groupmode' => 1,
 577                      'availability' => null));
 578          $mods[4] = $this->getDataGenerator()->create_module('forum',
 579                  array('course' => $course->id,
 580                      'grouping' => 12));
 581  
 582          $modinfo = get_fast_modinfo($course->id);
 583  
 584          // Make sure that object returned by get_course_module_record(false) has exactly the same fields as DB table 'course_modules'.
 585          $dbfields = array_keys($DB->get_columns('course_modules'));
 586          sort($dbfields);
 587          $cmrecord = $modinfo->get_cm($mods[0]->cmid)->get_course_module_record();
 588          $cmrecordfields = array_keys((array)$cmrecord);
 589          sort($cmrecordfields);
 590          $this->assertEquals($dbfields, $cmrecordfields);
 591  
 592          // Make sure that object returned by get_course_module_record(true) has exactly the same fields
 593          // as object returned by get_coursemodule_from_id(,,,true,);
 594          $cmrecordfull = $modinfo->get_cm($mods[0]->cmid)->get_course_module_record(true);
 595          $cmrecordfullfields = array_keys((array)$cmrecordfull);
 596          $cm = get_coursemodule_from_id(null, $mods[0]->cmid, 0, true, MUST_EXIST);
 597          $cmfields = array_keys((array)$cm);
 598          $this->assertEquals($cmfields, $cmrecordfullfields);
 599  
 600          // Make sure that object returned by get_course_module_record(true) has exactly the same fields
 601          // as object returned by get_coursemodule_from_instance(,,,true,);
 602          $cm = get_coursemodule_from_instance('forum', $mods[0]->id, null, true, MUST_EXIST);
 603          $cmfields = array_keys((array)$cm);
 604          $this->assertEquals($cmfields, $cmrecordfullfields);
 605  
 606          // Make sure the objects have the same properties.
 607          $cm1 = get_coursemodule_from_id(null, $mods[0]->cmid, 0, true, MUST_EXIST);
 608          $cm2 = get_coursemodule_from_instance('forum', $mods[0]->id, 0, true, MUST_EXIST);
 609          $cminfo = $modinfo->get_cm($mods[0]->cmid);
 610          $record = $DB->get_record('course_modules', array('id' => $mods[0]->cmid));
 611          $this->assertEquals($record, $cminfo->get_course_module_record());
 612          $this->assertEquals($cm1, $cminfo->get_course_module_record(true));
 613          $this->assertEquals($cm2, $cminfo->get_course_module_record(true));
 614  
 615          $cm1 = get_coursemodule_from_id(null, $mods[1]->cmid, 0, true, MUST_EXIST);
 616          $cm2 = get_coursemodule_from_instance('assign', $mods[1]->id, 0, true, MUST_EXIST);
 617          $cminfo = $modinfo->get_cm($mods[1]->cmid);
 618          $record = $DB->get_record('course_modules', array('id' => $mods[1]->cmid));
 619          $this->assertEquals($record, $cminfo->get_course_module_record());
 620          $this->assertEquals($cm1, $cminfo->get_course_module_record(true));
 621          $this->assertEquals($cm2, $cminfo->get_course_module_record(true));
 622  
 623          $cm1 = get_coursemodule_from_id(null, $mods[2]->cmid, 0, true, MUST_EXIST);
 624          $cm2 = get_coursemodule_from_instance('book', $mods[2]->id, 0, true, MUST_EXIST);
 625          $cminfo = $modinfo->get_cm($mods[2]->cmid);
 626          $record = $DB->get_record('course_modules', array('id' => $mods[2]->cmid));
 627          $this->assertEquals($record, $cminfo->get_course_module_record());
 628          $this->assertEquals($cm1, $cminfo->get_course_module_record(true));
 629          $this->assertEquals($cm2, $cminfo->get_course_module_record(true));
 630  
 631          $cm1 = get_coursemodule_from_id(null, $mods[3]->cmid, 0, true, MUST_EXIST);
 632          $cm2 = get_coursemodule_from_instance('forum', $mods[3]->id, 0, true, MUST_EXIST);
 633          $cminfo = $modinfo->get_cm($mods[3]->cmid);
 634          $record = $DB->get_record('course_modules', array('id' => $mods[3]->cmid));
 635          $this->assertEquals($record, $cminfo->get_course_module_record());
 636          $this->assertEquals($cm1, $cminfo->get_course_module_record(true));
 637          $this->assertEquals($cm2, $cminfo->get_course_module_record(true));
 638  
 639          $cm1 = get_coursemodule_from_id(null, $mods[4]->cmid, 0, true, MUST_EXIST);
 640          $cm2 = get_coursemodule_from_instance('forum', $mods[4]->id, 0, true, MUST_EXIST);
 641          $cminfo = $modinfo->get_cm($mods[4]->cmid);
 642          $record = $DB->get_record('course_modules', array('id' => $mods[4]->cmid));
 643          $this->assertEquals($record, $cminfo->get_course_module_record());
 644          $this->assertEquals($cm1, $cminfo->get_course_module_record(true));
 645          $this->assertEquals($cm2, $cminfo->get_course_module_record(true));
 646  
 647      }
 648  
 649      /**
 650       * Tests for function cm_info::get_activitybadge().
 651       *
 652       * @covers \cm_info::get_activitybadge
 653       */
 654      public function test_cm_info_get_activitybadge(): void {
 655          global $PAGE;
 656  
 657          $this->resetAfterTest();
 658          $this->setAdminUser();
 659  
 660          $course = $this->getDataGenerator()->create_course();
 661          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 662          $resource = $this->getDataGenerator()->create_module('resource', ['course' => $course->id]);
 663          $assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
 664          $label = $this->getDataGenerator()->create_module('label', ['course' => $course->id]);
 665  
 666          $renderer = $PAGE->get_renderer('core');
 667          $modinfo = get_fast_modinfo($course->id);
 668  
 669          // Forum and resource implements the activitybadge feature.
 670          $cminfo = $modinfo->get_cm($forum->cmid);
 671          $this->assertNotNull($cminfo->get_activitybadge($renderer));
 672          $cminfo = $modinfo->get_cm($resource->cmid);
 673          $this->assertNotNull($cminfo->get_activitybadge($renderer));
 674  
 675          // Assign and label don't implement the activitybadge feature (at least for now).
 676          $cminfo = $modinfo->get_cm($assign->cmid);
 677          $this->assertNull($cminfo->get_activitybadge($renderer));
 678          $cminfo = $modinfo->get_cm($label->cmid);
 679          $this->assertNull($cminfo->get_activitybadge($renderer));
 680      }
 681  
 682      /**
 683       * Tests the availability property that has been added to course modules
 684       * and sections (just to see that it is correctly saved and accessed).
 685       */
 686      public function test_availability_property() {
 687          global $DB, $CFG;
 688  
 689          $this->resetAfterTest();
 690  
 691          // Create a course with two modules and three sections.
 692          $course = $this->getDataGenerator()->create_course(
 693                  array('format' => 'topics', 'numsections' => 3),
 694                  array('createsections' => true));
 695          $forum = $this->getDataGenerator()->create_module('forum',
 696                  array('course' => $course->id));
 697          $forum2 = $this->getDataGenerator()->create_module('forum',
 698                  array('course' => $course->id));
 699  
 700          // Get modinfo. Check that availability is null for both cm and sections.
 701          $modinfo = get_fast_modinfo($course->id);
 702          $cm = $modinfo->get_cm($forum->cmid);
 703          $this->assertNull($cm->availability);
 704          $section = $modinfo->get_section_info(1, MUST_EXIST);
 705          $this->assertNull($section->availability);
 706  
 707          // Update availability for cm and section in database.
 708          $DB->set_field('course_modules', 'availability', '{}', array('id' => $cm->id));
 709          $DB->set_field('course_sections', 'availability', '{}', array('id' => $section->id));
 710  
 711          // Clear cache and get modinfo again.
 712          rebuild_course_cache($course->id, true);
 713          get_fast_modinfo(0, 0, true);
 714          $modinfo = get_fast_modinfo($course->id);
 715  
 716          // Check values that were changed.
 717          $cm = $modinfo->get_cm($forum->cmid);
 718          $this->assertEquals('{}', $cm->availability);
 719          $section = $modinfo->get_section_info(1, MUST_EXIST);
 720          $this->assertEquals('{}', $section->availability);
 721  
 722          // Check other values are still null.
 723          $cm = $modinfo->get_cm($forum2->cmid);
 724          $this->assertNull($cm->availability);
 725          $section = $modinfo->get_section_info(2, MUST_EXIST);
 726          $this->assertNull($section->availability);
 727      }
 728  
 729      /**
 730       * Tests for get_groups() method.
 731       */
 732      public function test_get_groups() {
 733          $this->resetAfterTest();
 734          $generator = $this->getDataGenerator();
 735  
 736          // Create courses.
 737          $course1 = $generator->create_course();
 738          $course2 = $generator->create_course();
 739          $course3 = $generator->create_course();
 740  
 741          // Create users.
 742          $user1 = $generator->create_user();
 743          $user2 = $generator->create_user();
 744          $user3 = $generator->create_user();
 745  
 746          // Enrol users on courses.
 747          $generator->enrol_user($user1->id, $course1->id);
 748          $generator->enrol_user($user2->id, $course2->id);
 749          $generator->enrol_user($user3->id, $course2->id);
 750          $generator->enrol_user($user3->id, $course3->id);
 751  
 752          // Create groups.
 753          $group1 = $generator->create_group(array('courseid' => $course1->id));
 754          $group2 = $generator->create_group(array('courseid' => $course2->id));
 755          $group3 = $generator->create_group(array('courseid' => $course2->id));
 756  
 757          // Assign users to groups and assert the result.
 758          $this->assertTrue($generator->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id)));
 759          $this->assertTrue($generator->create_group_member(array('groupid' => $group2->id, 'userid' => $user2->id)));
 760          $this->assertTrue($generator->create_group_member(array('groupid' => $group3->id, 'userid' => $user2->id)));
 761          $this->assertTrue($generator->create_group_member(array('groupid' => $group2->id, 'userid' => $user3->id)));
 762  
 763          // Create groupings.
 764          $grouping1 = $generator->create_grouping(array('courseid' => $course1->id));
 765          $grouping2 = $generator->create_grouping(array('courseid' => $course2->id));
 766  
 767          // Assign and assert group to groupings.
 768          groups_assign_grouping($grouping1->id, $group1->id);
 769          groups_assign_grouping($grouping2->id, $group2->id);
 770          groups_assign_grouping($grouping2->id, $group3->id);
 771  
 772          // Test with one single group.
 773          $modinfo = get_fast_modinfo($course1, $user1->id);
 774          $groups = $modinfo->get_groups($grouping1->id);
 775          $this->assertCount(1, $groups);
 776          $this->assertArrayHasKey($group1->id, $groups);
 777  
 778          // Test with two groups.
 779          $modinfo = get_fast_modinfo($course2, $user2->id);
 780          $groups = $modinfo->get_groups();
 781          $this->assertCount(2, $groups);
 782          $this->assertTrue(in_array($group2->id, $groups));
 783          $this->assertTrue(in_array($group3->id, $groups));
 784  
 785          // Test with no groups.
 786          $modinfo = get_fast_modinfo($course3, $user3->id);
 787          $groups = $modinfo->get_groups();
 788          $this->assertCount(0, $groups);
 789          $this->assertArrayNotHasKey($group1->id, $groups);
 790      }
 791  
 792      /**
 793       * Tests the function for constructing a cm_info from mixed data.
 794       */
 795      public function test_create() {
 796          global $CFG, $DB;
 797          $this->resetAfterTest();
 798  
 799          // Create a course and an activity.
 800          $generator = $this->getDataGenerator();
 801          $course = $generator->create_course();
 802          $page = $generator->create_module('page', array('course' => $course->id,
 803                  'name' => 'Annie'));
 804  
 805          // Null is passed through.
 806          $this->assertNull(cm_info::create(null));
 807  
 808          // Stdclass object turns into cm_info.
 809          $cm = cm_info::create(
 810                  (object)array('id' => $page->cmid, 'course' => $course->id));
 811          $this->assertInstanceOf('cm_info', $cm);
 812          $this->assertEquals('Annie', $cm->name);
 813  
 814          // A cm_info object stays as cm_info.
 815          $this->assertSame($cm, cm_info::create($cm));
 816  
 817          // Invalid object (missing fields) causes error.
 818          try {
 819              cm_info::create((object)array('id' => $page->cmid));
 820              $this->fail();
 821          } catch (Exception $e) {
 822              $this->assertInstanceOf('coding_exception', $e);
 823          }
 824  
 825          // Create a second hidden activity.
 826          $hiddenpage = $generator->create_module('page', array('course' => $course->id,
 827                  'name' => 'Annie', 'visible' => 0));
 828  
 829          // Create 2 user accounts, one is a manager who can see everything.
 830          $user = $generator->create_user();
 831          $generator->enrol_user($user->id, $course->id);
 832          $manager = $generator->create_user();
 833          $generator->enrol_user($manager->id, $course->id,
 834                  $DB->get_field('role', 'id', array('shortname' => 'manager'), MUST_EXIST));
 835  
 836          // User can see the normal page but not the hidden one.
 837          $cm = cm_info::create((object)array('id' => $page->cmid, 'course' => $course->id),
 838                  $user->id);
 839          $this->assertTrue($cm->uservisible);
 840          $cm = cm_info::create((object)array('id' => $hiddenpage->cmid, 'course' => $course->id),
 841                  $user->id);
 842          $this->assertFalse($cm->uservisible);
 843  
 844          // Manager can see the hidden one too.
 845          $cm = cm_info::create((object)array('id' => $hiddenpage->cmid, 'course' => $course->id),
 846                  $manager->id);
 847          $this->assertTrue($cm->uservisible);
 848      }
 849  
 850      /**
 851       * Tests function for getting $course and $cm at once quickly from modinfo
 852       * based on cmid or cm record.
 853       */
 854      public function test_get_course_and_cm_from_cmid() {
 855          global $CFG, $DB;
 856          $this->resetAfterTest();
 857  
 858          // Create a course and an activity.
 859          $generator = $this->getDataGenerator();
 860          $course = $generator->create_course(array('shortname' => 'Halls'));
 861          $page = $generator->create_module('page', array('course' => $course->id,
 862                  'name' => 'Annie'));
 863  
 864          // Successful usage.
 865          list($course, $cm) = get_course_and_cm_from_cmid($page->cmid);
 866          $this->assertEquals('Halls', $course->shortname);
 867          $this->assertInstanceOf('cm_info', $cm);
 868          $this->assertEquals('Annie', $cm->name);
 869  
 870          // Specified module type.
 871          list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page');
 872          $this->assertEquals('Annie', $cm->name);
 873  
 874          // With id in object.
 875          $fakecm = (object)array('id' => $page->cmid);
 876          list($course, $cm) = get_course_and_cm_from_cmid($fakecm);
 877          $this->assertEquals('Halls', $course->shortname);
 878          $this->assertEquals('Annie', $cm->name);
 879  
 880          // With both id and course in object.
 881          $fakecm->course = $course->id;
 882          list($course, $cm) = get_course_and_cm_from_cmid($fakecm);
 883          $this->assertEquals('Halls', $course->shortname);
 884          $this->assertEquals('Annie', $cm->name);
 885  
 886          // With supplied course id.
 887          list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', $course->id);
 888          $this->assertEquals('Annie', $cm->name);
 889  
 890          // With supplied course object (modified just so we can check it is
 891          // indeed reusing the supplied object).
 892          $course->silly = true;
 893          list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', $course);
 894          $this->assertEquals('Annie', $cm->name);
 895          $this->assertTrue($course->silly);
 896  
 897          // Incorrect module type.
 898          try {
 899              get_course_and_cm_from_cmid($page->cmid, 'forum');
 900              $this->fail();
 901          } catch (moodle_exception $e) {
 902              $this->assertEquals('invalidcoursemoduleid', $e->errorcode);
 903          }
 904  
 905          // Invalid module name.
 906          try {
 907              get_course_and_cm_from_cmid($page->cmid, 'pigs can fly');
 908              $this->fail();
 909          } catch (coding_exception $e) {
 910              $this->assertStringContainsString('Invalid modulename parameter', $e->getMessage());
 911          }
 912  
 913          // Doesn't exist.
 914          try {
 915              get_course_and_cm_from_cmid($page->cmid + 1);
 916              $this->fail();
 917          } catch (moodle_exception $e) {
 918              $this->assertInstanceOf('dml_exception', $e);
 919          }
 920  
 921          // Create a second hidden activity.
 922          $hiddenpage = $generator->create_module('page', array('course' => $course->id,
 923                  'name' => 'Annie', 'visible' => 0));
 924  
 925          // Create 2 user accounts, one is a manager who can see everything.
 926          $user = $generator->create_user();
 927          $generator->enrol_user($user->id, $course->id);
 928          $manager = $generator->create_user();
 929          $generator->enrol_user($manager->id, $course->id,
 930                  $DB->get_field('role', 'id', array('shortname' => 'manager'), MUST_EXIST));
 931  
 932          // User can see the normal page but not the hidden one.
 933          list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', 0, $user->id);
 934          $this->assertTrue($cm->uservisible);
 935          list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $user->id);
 936          $this->assertFalse($cm->uservisible);
 937  
 938          // Manager can see the hidden one too.
 939          list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $manager->id);
 940          $this->assertTrue($cm->uservisible);
 941      }
 942  
 943      /**
 944       * Tests function for getting $course and $cm at once quickly from modinfo
 945       * based on instance id or record.
 946       */
 947      public function test_get_course_and_cm_from_instance() {
 948          global $CFG, $DB;
 949          $this->resetAfterTest();
 950  
 951          // Create a course and an activity.
 952          $generator = $this->getDataGenerator();
 953          $course = $generator->create_course(array('shortname' => 'Halls'));
 954          $page = $generator->create_module('page', array('course' => $course->id,
 955                  'name' => 'Annie'));
 956  
 957          // Successful usage.
 958          list($course, $cm) = get_course_and_cm_from_instance($page->id, 'page');
 959          $this->assertEquals('Halls', $course->shortname);
 960          $this->assertInstanceOf('cm_info', $cm);
 961          $this->assertEquals('Annie', $cm->name);
 962  
 963          // With id in object.
 964          $fakeinstance = (object)array('id' => $page->id);
 965          list($course, $cm) = get_course_and_cm_from_instance($fakeinstance, 'page');
 966          $this->assertEquals('Halls', $course->shortname);
 967          $this->assertEquals('Annie', $cm->name);
 968  
 969          // With both id and course in object.
 970          $fakeinstance->course = $course->id;
 971          list($course, $cm) = get_course_and_cm_from_instance($fakeinstance, 'page');
 972          $this->assertEquals('Halls', $course->shortname);
 973          $this->assertEquals('Annie', $cm->name);
 974  
 975          // With supplied course id.
 976          list($course, $cm) = get_course_and_cm_from_instance($page->id, 'page', $course->id);
 977          $this->assertEquals('Annie', $cm->name);
 978  
 979          // With supplied course object (modified just so we can check it is
 980          // indeed reusing the supplied object).
 981          $course->silly = true;
 982          list($course, $cm) = get_course_and_cm_from_instance($page->id, 'page', $course);
 983          $this->assertEquals('Annie', $cm->name);
 984          $this->assertTrue($course->silly);
 985  
 986          // Doesn't exist (or is wrong type).
 987          try {
 988              get_course_and_cm_from_instance($page->id, 'forum');
 989              $this->fail();
 990          } catch (moodle_exception $e) {
 991              $this->assertInstanceOf('dml_exception', $e);
 992          }
 993  
 994          // Invalid module name.
 995          try {
 996              get_course_and_cm_from_cmid($page->cmid, '1337 h4x0ring');
 997              $this->fail();
 998          } catch (coding_exception $e) {
 999              $this->assertStringContainsString('Invalid modulename parameter', $e->getMessage());
1000          }
1001  
1002          // Create a second hidden activity.
1003          $hiddenpage = $generator->create_module('page', array('course' => $course->id,
1004                  'name' => 'Annie', 'visible' => 0));
1005  
1006          // Create 2 user accounts, one is a manager who can see everything.
1007          $user = $generator->create_user();
1008          $generator->enrol_user($user->id, $course->id);
1009          $manager = $generator->create_user();
1010          $generator->enrol_user($manager->id, $course->id,
1011                  $DB->get_field('role', 'id', array('shortname' => 'manager'), MUST_EXIST));
1012  
1013          // User can see the normal page but not the hidden one.
1014          list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', 0, $user->id);
1015          $this->assertTrue($cm->uservisible);
1016          list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $user->id);
1017          $this->assertFalse($cm->uservisible);
1018  
1019          // Manager can see the hidden one too.
1020          list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $manager->id);
1021          $this->assertTrue($cm->uservisible);
1022      }
1023  
1024      /**
1025       * Test test_get_section_info_by_id method
1026       *
1027       * @dataProvider get_section_info_by_id_provider
1028       * @covers \course_modinfo::get_section_info_by_id
1029       *
1030       * @param int $sectionnum the section number
1031       * @param int $strictness the search strict mode
1032       * @param bool $expectnull if the function will return a null
1033       * @param bool $expectexception if the function will throw an exception
1034       */
1035      public function test_get_section_info_by_id(
1036          int $sectionnum,
1037          int $strictness = IGNORE_MISSING,
1038          bool $expectnull = false,
1039          bool $expectexception = false
1040      ) {
1041          global $DB;
1042  
1043          $this->resetAfterTest();
1044  
1045          // Create a course with 4 sections.
1046          $course = $this->getDataGenerator()->create_course(['numsections' => 4]);
1047  
1048          // Index sections.
1049          $sectionindex = [];
1050          $modinfo = get_fast_modinfo($course);
1051          $allsections = $modinfo->get_section_info_all();
1052          foreach ($allsections as $section) {
1053              $sectionindex[$section->section] = $section->id;
1054          }
1055  
1056          if ($expectexception) {
1057              $this->expectException(moodle_exception::class);
1058          }
1059  
1060          $sectionid = $sectionindex[$sectionnum] ?? -1;
1061  
1062          $section = $modinfo->get_section_info_by_id($sectionid, $strictness);
1063  
1064          if ($expectnull) {
1065              $this->assertNull($section);
1066          } else {
1067              $this->assertEquals($sectionid, $section->id);
1068              $this->assertEquals($sectionnum, $section->section);
1069          }
1070      }
1071  
1072      /**
1073       * Data provider for test_get_section_info_by_id().
1074       *
1075       * @return array
1076       */
1077      public function get_section_info_by_id_provider() {
1078          return [
1079              'Valid section id' => [
1080                  'sectionnum' => 1,
1081                  'strictness' => IGNORE_MISSING,
1082                  'expectnull' => false,
1083                  'expectexception' => false,
1084              ],
1085              'Section zero' => [
1086                  'sectionnum' => 0,
1087                  'strictness' => IGNORE_MISSING,
1088                  'expectnull' => false,
1089                  'expectexception' => false,
1090              ],
1091              'invalid section ignore missing' => [
1092                  'sectionnum' => -1,
1093                  'strictness' => IGNORE_MISSING,
1094                  'expectnull' => true,
1095                  'expectexception' => false,
1096              ],
1097              'invalid section must exists' => [
1098                  'sectionnum' => -1,
1099                  'strictness' => MUST_EXIST,
1100                  'expectnull' => false,
1101                  'expectexception' => true,
1102              ],
1103          ];
1104      }
1105  
1106      /**
1107       * Test purge_section_cache_by_id method
1108       *
1109       * @covers \course_modinfo::purge_course_section_cache_by_id
1110       * @return void
1111       */
1112      public function test_purge_section_cache_by_id(): void {
1113          $this->resetAfterTest();
1114          $this->setAdminUser();
1115          $cache = cache::make('core', 'coursemodinfo');
1116  
1117          // Generate the course and pre-requisite section.
1118          $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 3], ['createsections' => true]);
1119          // Reset course cache.
1120          rebuild_course_cache($course->id, true);
1121          // Build course cache.
1122          get_fast_modinfo($course->id);
1123          // Get the course modinfo cache.
1124          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1125          // Get the section cache.
1126          $sectioncaches = $coursemodinfo->sectioncache;
1127  
1128          // Make sure that we will have 4 section caches here.
1129          $this->assertCount(4, $sectioncaches);
1130          $this->assertArrayHasKey(0, $sectioncaches);
1131          $this->assertArrayHasKey(1, $sectioncaches);
1132          $this->assertArrayHasKey(2, $sectioncaches);
1133          $this->assertArrayHasKey(3, $sectioncaches);
1134  
1135          // Purge cache for the section by id.
1136          course_modinfo::purge_course_section_cache_by_id($course->id, $sectioncaches[1]->id);
1137          // Get the course modinfo cache.
1138          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1139          // Get the section cache.
1140          $sectioncaches = $coursemodinfo->sectioncache;
1141  
1142          // Make sure that we will have 3 section caches left.
1143          $this->assertCount(3, $sectioncaches);
1144          $this->assertArrayNotHasKey(1, $sectioncaches);
1145          $this->assertArrayHasKey(0, $sectioncaches);
1146          $this->assertArrayHasKey(2, $sectioncaches);
1147          $this->assertArrayHasKey(3, $sectioncaches);
1148          // Make sure that the cacherev will be reset.
1149          $this->assertEquals(-1, $coursemodinfo->cacherev);
1150      }
1151  
1152      /**
1153       * Test purge_section_cache_by_number method
1154       *
1155       * @covers \course_modinfo::purge_course_section_cache_by_number
1156       * @return void
1157       */
1158      public function test_section_cache_by_number(): void {
1159          $this->resetAfterTest();
1160          $this->setAdminUser();
1161          $cache = cache::make('core', 'coursemodinfo');
1162  
1163          // Generate the course and pre-requisite section.
1164          $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 3], ['createsections' => true]);
1165          // Reset course cache.
1166          rebuild_course_cache($course->id, true);
1167          // Build course cache.
1168          get_fast_modinfo($course->id);
1169          // Get the course modinfo cache.
1170          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1171          // Get the section cache.
1172          $sectioncaches = $coursemodinfo->sectioncache;
1173  
1174          // Make sure that we will have 4 section caches here.
1175          $this->assertCount(4, $sectioncaches);
1176          $this->assertArrayHasKey(0, $sectioncaches);
1177          $this->assertArrayHasKey(1, $sectioncaches);
1178          $this->assertArrayHasKey(2, $sectioncaches);
1179          $this->assertArrayHasKey(3, $sectioncaches);
1180  
1181          // Purge cache for the section with section number is 1.
1182          course_modinfo::purge_course_section_cache_by_number($course->id, 1);
1183          // Get the course modinfo cache.
1184          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1185          // Get the section cache.
1186          $sectioncaches = $coursemodinfo->sectioncache;
1187  
1188          // Make sure that we will have 3 section caches left.
1189          $this->assertCount(3, $sectioncaches);
1190          $this->assertArrayNotHasKey(1, $sectioncaches);
1191          $this->assertArrayHasKey(0, $sectioncaches);
1192          $this->assertArrayHasKey(2, $sectioncaches);
1193          $this->assertArrayHasKey(3, $sectioncaches);
1194          // Make sure that the cacherev will be reset.
1195          $this->assertEquals(-1, $coursemodinfo->cacherev);
1196      }
1197  
1198      /**
1199       * Purge a single course module from the cache.
1200       *
1201       * @return void
1202       * @covers \course_modinfo::purge_course_module_cache
1203       */
1204      public function test_purge_course_module(): void {
1205          $this->resetAfterTest();
1206          $this->setAdminUser();
1207          $cache = cache::make('core', 'coursemodinfo');
1208  
1209          // Generate the course and pre-requisite section.
1210          $course = $this->getDataGenerator()->create_course();
1211          $cm1 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
1212          $cm2 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
1213          $cm3 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
1214          $cm4 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
1215          // Reset course cache.
1216          rebuild_course_cache($course->id, true);
1217          // Build course cache.
1218          get_fast_modinfo($course->id);
1219          // Get the course modinfo cache.
1220          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1221          $this->assertCount(4, $coursemodinfo->modinfo);
1222          $this->assertArrayHasKey($cm1->cmid, $coursemodinfo->modinfo);
1223          $this->assertArrayHasKey($cm2->cmid, $coursemodinfo->modinfo);
1224          $this->assertArrayHasKey($cm3->cmid, $coursemodinfo->modinfo);
1225          $this->assertArrayHasKey($cm4->cmid, $coursemodinfo->modinfo);
1226  
1227          course_modinfo::purge_course_module_cache($course->id, $cm1->cmid);
1228  
1229          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1230          $this->assertCount(3, $coursemodinfo->modinfo);
1231          $this->assertArrayNotHasKey($cm1->cmid, $coursemodinfo->modinfo);
1232          $this->assertArrayHasKey($cm2->cmid, $coursemodinfo->modinfo);
1233          $this->assertArrayHasKey($cm3->cmid, $coursemodinfo->modinfo);
1234          $this->assertArrayHasKey($cm4->cmid, $coursemodinfo->modinfo);
1235          // Make sure that the cacherev will be reset.
1236          $this->assertEquals(-1, $coursemodinfo->cacherev);
1237      }
1238  
1239      /**
1240       * Purge a multiple course modules from the cache.
1241       *
1242       * @return void
1243       * @covers \course_modinfo::purge_course_modules_cache
1244       */
1245      public function test_purge_multiple_course_modules(): void {
1246          $this->resetAfterTest();
1247          $this->setAdminUser();
1248          $cache = cache::make('core', 'coursemodinfo');
1249  
1250          // Generate the course and pre-requisite section.
1251          $course = $this->getDataGenerator()->create_course();
1252          $cm1 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
1253          $cm2 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
1254          $cm3 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
1255          $cm4 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
1256          // Reset course cache.
1257          rebuild_course_cache($course->id, true);
1258          // Build course cache.
1259          get_fast_modinfo($course->id);
1260          // Get the course modinfo cache.
1261          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1262          $this->assertCount(4, $coursemodinfo->modinfo);
1263          $this->assertArrayHasKey($cm1->cmid, $coursemodinfo->modinfo);
1264          $this->assertArrayHasKey($cm2->cmid, $coursemodinfo->modinfo);
1265          $this->assertArrayHasKey($cm3->cmid, $coursemodinfo->modinfo);
1266          $this->assertArrayHasKey($cm4->cmid, $coursemodinfo->modinfo);
1267  
1268          course_modinfo::purge_course_modules_cache($course->id, [$cm2->cmid, $cm3->cmid]);
1269  
1270          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1271          $this->assertCount(2, $coursemodinfo->modinfo);
1272          $this->assertArrayHasKey($cm1->cmid, $coursemodinfo->modinfo);
1273          $this->assertArrayNotHasKey($cm2->cmid, $coursemodinfo->modinfo);
1274          $this->assertArrayNotHasKey($cm3->cmid, $coursemodinfo->modinfo);
1275          $this->assertArrayHasKey($cm4->cmid, $coursemodinfo->modinfo);
1276          // Make sure that the cacherev will be reset.
1277          $this->assertEquals(-1, $coursemodinfo->cacherev);
1278      }
1279  
1280      /**
1281       * Test get_cm() method to output course module id in the exception text.
1282       *
1283       * @covers \course_modinfo::get_cm
1284       * @return void
1285       */
1286      public function test_invalid_course_module_id(): void {
1287          global $DB;
1288          $this->resetAfterTest();
1289  
1290          $course = $this->getDataGenerator()->create_course();
1291          $forum0 = $this->getDataGenerator()->create_module('assign', ['course' => $course->id], ['section' => 0]);
1292          $forum1 = $this->getDataGenerator()->create_module('assign', ['course' => $course->id], ['section' => 0]);
1293          $forum2 = $this->getDataGenerator()->create_module('assign', ['course' => $course->id], ['section' => 0]);
1294  
1295          // Break section sequence.
1296          $modinfo = get_fast_modinfo($course->id);
1297          $sectionid = $modinfo->get_section_info(0)->id;
1298          $section = $DB->get_record('course_sections', ['id' => $sectionid]);
1299          $sequence = explode(',', $section->sequence);
1300          $sequence = array_diff($sequence, [$forum1->cmid]);
1301          $section->sequence = implode(',', $sequence);
1302          $DB->update_record('course_sections', $section);
1303  
1304          // Assert exception text.
1305          $this->expectException(\moodle_exception::class);
1306          $this->expectExceptionMessage('Invalid course module ID: ' . $forum1->cmid);
1307          delete_course($course, false);
1308      }
1309  
1310      /**
1311       * Tests that if the modinfo cache returns a newer-than-expected version, Moodle won't rebuild
1312       * it.
1313       *
1314       * This is important to avoid wasted time/effort and poor performance, for example in cases
1315       * where multiple requests are accessing the course.
1316       *
1317       * Certain cases could be particularly bad if this test fails. For example, if using clustered
1318       * databases where there is a 100ms delay between updates to the course table being available
1319       * to all users (but no such delay on the cache infrastructure), then during that 100ms, every
1320       * request that calls get_fast_modinfo and uses the read-only database will rebuild the course
1321       * cache. Since these will then create a still-newer version, future requests for the next
1322       * 100ms will also rebuild it again... etc.
1323       *
1324       * @covers \course_modinfo
1325       */
1326      public function test_get_modinfo_with_newer_version(): void {
1327          global $DB;
1328  
1329          $this->resetAfterTest();
1330  
1331          // Get info about a course and build the initial cache, then drop it from memory.
1332          $course = $this->getDataGenerator()->create_course();
1333          get_fast_modinfo($course);
1334          get_fast_modinfo(0, 0, true);
1335  
1336          // User A starts a request, which takes some time...
1337          $useracourse = $DB->get_record('course', ['id' => $course->id]);
1338  
1339          // User B also starts a request and makes a change to the course.
1340          $userbcourse = $DB->get_record('course', ['id' => $course->id]);
1341          $this->getDataGenerator()->create_module('page', ['course' => $course->id]);
1342          rebuild_course_cache($userbcourse->id, false);
1343  
1344          // Finally, user A's request now gets modinfo. It should accept the version from B even
1345          // though the course version (of cache) is newer than the one expected by A.
1346          $before = $DB->perf_get_queries();
1347          $modinfo = get_fast_modinfo($useracourse);
1348          $after = $DB->perf_get_queries();
1349          $this->assertEquals($after, $before, 'Should use cached version, making no DB queries');
1350  
1351          // Obviously, modinfo should include the Page now.
1352          $this->assertCount(1, $modinfo->get_instances_of('page'));
1353      }
1354  }