Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace 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      public function test_course_modinfo_properties() {
 338          global $USER, $DB;
 339  
 340          $this->resetAfterTest();
 341          $this->setAdminUser();
 342  
 343          // Generate the course and some modules. Make one section hidden.
 344          $course = $this->getDataGenerator()->create_course(
 345                  array('format' => 'topics',
 346                      'numsections' => 3),
 347                  array('createsections' => true));
 348          $DB->execute('UPDATE {course_sections} SET visible = 0 WHERE course = ? and section = ?',
 349                  array($course->id, 3));
 350          $coursecontext = context_course::instance($course->id);
 351          $forum0 = $this->getDataGenerator()->create_module('forum',
 352                  array('course' => $course->id), array('section' => 0));
 353          $assign0 = $this->getDataGenerator()->create_module('assign',
 354                  array('course' => $course->id), array('section' => 0, 'visible' => 0));
 355          $forum1 = $this->getDataGenerator()->create_module('forum',
 356                  array('course' => $course->id), array('section' => 1));
 357          $assign1 = $this->getDataGenerator()->create_module('assign',
 358                  array('course' => $course->id), array('section' => 1));
 359          $page1 = $this->getDataGenerator()->create_module('page',
 360                  array('course' => $course->id), array('section' => 1));
 361          $page3 = $this->getDataGenerator()->create_module('page',
 362                  array('course' => $course->id), array('section' => 3));
 363  
 364          $modinfo = get_fast_modinfo($course->id);
 365  
 366          $this->assertEquals(array($forum0->cmid, $assign0->cmid, $forum1->cmid, $assign1->cmid, $page1->cmid, $page3->cmid),
 367                  array_keys($modinfo->cms));
 368          $this->assertEquals($course->id, $modinfo->courseid);
 369          $this->assertEquals($USER->id, $modinfo->userid);
 370          $this->assertEquals(array(0 => array($forum0->cmid, $assign0->cmid),
 371              1 => array($forum1->cmid, $assign1->cmid, $page1->cmid), 3 => array($page3->cmid)), $modinfo->sections);
 372          $this->assertEquals(array('forum', 'assign', 'page'), array_keys($modinfo->instances));
 373          $this->assertEquals(array($assign0->id, $assign1->id), array_keys($modinfo->instances['assign']));
 374          $this->assertEquals(array($forum0->id, $forum1->id), array_keys($modinfo->instances['forum']));
 375          $this->assertEquals(array($page1->id, $page3->id), array_keys($modinfo->instances['page']));
 376          $this->assertEquals(groups_get_user_groups($course->id), $modinfo->groups);
 377          $this->assertEquals(array(0 => array($forum0->cmid, $assign0->cmid),
 378              1 => array($forum1->cmid, $assign1->cmid, $page1->cmid),
 379              3 => array($page3->cmid)), $modinfo->get_sections());
 380          $this->assertEquals(array(0, 1, 2, 3), array_keys($modinfo->get_section_info_all()));
 381          $this->assertEquals($forum0->cmid . ',' . $assign0->cmid, $modinfo->get_section_info(0)->sequence);
 382          $this->assertEquals($forum1->cmid . ',' . $assign1->cmid . ',' . $page1->cmid, $modinfo->get_section_info(1)->sequence);
 383          $this->assertEquals('', $modinfo->get_section_info(2)->sequence);
 384          $this->assertEquals($page3->cmid, $modinfo->get_section_info(3)->sequence);
 385          $this->assertEquals($course->id, $modinfo->get_course()->id);
 386          $names = array_keys($modinfo->get_used_module_names());
 387          sort($names);
 388          $this->assertEquals(array('assign', 'forum', 'page'), $names);
 389          $names = array_keys($modinfo->get_used_module_names(true));
 390          sort($names);
 391          $this->assertEquals(array('assign', 'forum', 'page'), $names);
 392          // Admin can see hidden modules/sections.
 393          $this->assertTrue($modinfo->cms[$assign0->cmid]->uservisible);
 394          $this->assertTrue($modinfo->get_section_info(3)->uservisible);
 395  
 396          // Get modinfo for non-current user (without capability to view hidden activities/sections).
 397          $user = $this->getDataGenerator()->create_user();
 398          $modinfo = get_fast_modinfo($course->id, $user->id);
 399          $this->assertEquals($user->id, $modinfo->userid);
 400          $this->assertFalse($modinfo->cms[$assign0->cmid]->uservisible);
 401          $this->assertFalse($modinfo->get_section_info(3)->uservisible);
 402  
 403          // Attempt to access and set non-existing field.
 404          $this->assertTrue(empty($modinfo->somefield));
 405          $this->assertFalse(isset($modinfo->somefield));
 406          $modinfo->somefield;
 407          $this->assertDebuggingCalled();
 408          $modinfo->somefield = 'Some value';
 409          $this->assertDebuggingCalled();
 410          $this->assertEmpty($modinfo->somefield);
 411          $this->assertDebuggingCalled();
 412  
 413          // Attempt to overwrite existing field.
 414          $this->assertFalse(empty($modinfo->cms));
 415          $this->assertTrue(isset($modinfo->cms));
 416          $modinfo->cms = 'Illegal overwriting';
 417          $this->assertDebuggingCalled();
 418          $this->assertNotEquals('Illegal overwriting', $modinfo->cms);
 419      }
 420  
 421      public function test_is_user_access_restricted_by_capability() {
 422          global $DB;
 423  
 424          $this->resetAfterTest();
 425  
 426          // Create a course and a mod_assign instance.
 427          $course = $this->getDataGenerator()->create_course();
 428          $assign = $this->getDataGenerator()->create_module('assign', array('course'=>$course->id));
 429  
 430          // Create and enrol a student.
 431          $coursecontext = context_course::instance($course->id);
 432          $studentrole = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
 433          $student = $this->getDataGenerator()->create_user();
 434          role_assign($studentrole->id, $student->id, $coursecontext);
 435          $enrolplugin = enrol_get_plugin('manual');
 436          $enrolinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual'));
 437          $enrolplugin->enrol_user($enrolinstance, $student->id);
 438          $this->setUser($student);
 439  
 440          // Make sure student can see the module.
 441          $cm = get_fast_modinfo($course->id)->instances['assign'][$assign->id];
 442          $this->assertTrue($cm->uservisible);
 443          $this->assertFalse($cm->is_user_access_restricted_by_capability());
 444  
 445          // Prohibit student to view mod_assign for the course.
 446          role_change_permission($studentrole->id, $coursecontext, 'mod/assign:view', CAP_PROHIBIT);
 447          get_fast_modinfo($course->id, 0, true);
 448          $cm = get_fast_modinfo($course->id)->instances['assign'][$assign->id];
 449          $this->assertFalse($cm->uservisible);
 450          $this->assertTrue($cm->is_user_access_restricted_by_capability());
 451  
 452          // Restore permission to student to view mod_assign for the course.
 453          role_change_permission($studentrole->id, $coursecontext, 'mod/assign:view', CAP_INHERIT);
 454          get_fast_modinfo($course->id, 0, true);
 455          $cm = get_fast_modinfo($course->id)->instances['assign'][$assign->id];
 456          $this->assertTrue($cm->uservisible);
 457          $this->assertFalse($cm->is_user_access_restricted_by_capability());
 458  
 459          // Prohibit student to view mod_assign for the particular module.
 460          role_change_permission($studentrole->id, context_module::instance($cm->id), 'mod/assign:view', CAP_PROHIBIT);
 461          get_fast_modinfo($course->id, 0, true);
 462          $cm = get_fast_modinfo($course->id)->instances['assign'][$assign->id];
 463          $this->assertFalse($cm->uservisible);
 464          $this->assertTrue($cm->is_user_access_restricted_by_capability());
 465  
 466          // Check calling get_fast_modinfo() for different user:
 467          $this->setAdminUser();
 468          $cm = get_fast_modinfo($course->id)->instances['assign'][$assign->id];
 469          $this->assertTrue($cm->uservisible);
 470          $this->assertFalse($cm->is_user_access_restricted_by_capability());
 471          $cm = get_fast_modinfo($course->id, $student->id)->instances['assign'][$assign->id];
 472          $this->assertFalse($cm->uservisible);
 473          $this->assertTrue($cm->is_user_access_restricted_by_capability());
 474      }
 475  
 476      /**
 477       * Tests for function cm_info::get_course_module_record()
 478       */
 479      public function test_cm_info_get_course_module_record() {
 480          global $DB;
 481  
 482          $this->resetAfterTest();
 483          $this->setAdminUser();
 484  
 485          set_config('enableavailability', 1);
 486          set_config('enablecompletion', 1);
 487  
 488          $course = $this->getDataGenerator()->create_course(
 489                  array('format' => 'topics', 'numsections' => 3, 'enablecompletion' => 1),
 490                  array('createsections' => true));
 491          $mods = array();
 492          $mods[0] = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
 493          $mods[1] = $this->getDataGenerator()->create_module('assign',
 494                  array('course' => $course->id,
 495                      'section' => 3,
 496                      'idnumber' => '12345',
 497                      'showdescription' => true
 498                      ));
 499          // Pick a small valid availability value to use.
 500          $availabilityvalue = '{"op":"|","show":true,"c":[{"type":"date","d":">=","t":4}]}';
 501          $mods[2] = $this->getDataGenerator()->create_module('book',
 502                  array('course' => $course->id,
 503                      'indent' => 5,
 504                      'availability' => $availabilityvalue,
 505                      'showdescription' => false,
 506                      'completion' => true,
 507                      'completionview' => true,
 508                      'completionexpected' => time() + 5000,
 509                      ));
 510          $mods[3] = $this->getDataGenerator()->create_module('forum',
 511                  array('course' => $course->id,
 512                      'visible' => 0,
 513                      'groupmode' => 1,
 514                      'availability' => null));
 515          $mods[4] = $this->getDataGenerator()->create_module('forum',
 516                  array('course' => $course->id,
 517                      'grouping' => 12));
 518  
 519          $modinfo = get_fast_modinfo($course->id);
 520  
 521          // Make sure that object returned by get_course_module_record(false) has exactly the same fields as DB table 'course_modules'.
 522          $dbfields = array_keys($DB->get_columns('course_modules'));
 523          sort($dbfields);
 524          $cmrecord = $modinfo->get_cm($mods[0]->cmid)->get_course_module_record();
 525          $cmrecordfields = array_keys((array)$cmrecord);
 526          sort($cmrecordfields);
 527          $this->assertEquals($dbfields, $cmrecordfields);
 528  
 529          // Make sure that object returned by get_course_module_record(true) has exactly the same fields
 530          // as object returned by get_coursemodule_from_id(,,,true,);
 531          $cmrecordfull = $modinfo->get_cm($mods[0]->cmid)->get_course_module_record(true);
 532          $cmrecordfullfields = array_keys((array)$cmrecordfull);
 533          $cm = get_coursemodule_from_id(null, $mods[0]->cmid, 0, true, MUST_EXIST);
 534          $cmfields = array_keys((array)$cm);
 535          $this->assertEquals($cmfields, $cmrecordfullfields);
 536  
 537          // Make sure that object returned by get_course_module_record(true) has exactly the same fields
 538          // as object returned by get_coursemodule_from_instance(,,,true,);
 539          $cm = get_coursemodule_from_instance('forum', $mods[0]->id, null, true, MUST_EXIST);
 540          $cmfields = array_keys((array)$cm);
 541          $this->assertEquals($cmfields, $cmrecordfullfields);
 542  
 543          // Make sure the objects have the same properties.
 544          $cm1 = get_coursemodule_from_id(null, $mods[0]->cmid, 0, true, MUST_EXIST);
 545          $cm2 = get_coursemodule_from_instance('forum', $mods[0]->id, 0, true, MUST_EXIST);
 546          $cminfo = $modinfo->get_cm($mods[0]->cmid);
 547          $record = $DB->get_record('course_modules', array('id' => $mods[0]->cmid));
 548          $this->assertEquals($record, $cminfo->get_course_module_record());
 549          $this->assertEquals($cm1, $cminfo->get_course_module_record(true));
 550          $this->assertEquals($cm2, $cminfo->get_course_module_record(true));
 551  
 552          $cm1 = get_coursemodule_from_id(null, $mods[1]->cmid, 0, true, MUST_EXIST);
 553          $cm2 = get_coursemodule_from_instance('assign', $mods[1]->id, 0, true, MUST_EXIST);
 554          $cminfo = $modinfo->get_cm($mods[1]->cmid);
 555          $record = $DB->get_record('course_modules', array('id' => $mods[1]->cmid));
 556          $this->assertEquals($record, $cminfo->get_course_module_record());
 557          $this->assertEquals($cm1, $cminfo->get_course_module_record(true));
 558          $this->assertEquals($cm2, $cminfo->get_course_module_record(true));
 559  
 560          $cm1 = get_coursemodule_from_id(null, $mods[2]->cmid, 0, true, MUST_EXIST);
 561          $cm2 = get_coursemodule_from_instance('book', $mods[2]->id, 0, true, MUST_EXIST);
 562          $cminfo = $modinfo->get_cm($mods[2]->cmid);
 563          $record = $DB->get_record('course_modules', array('id' => $mods[2]->cmid));
 564          $this->assertEquals($record, $cminfo->get_course_module_record());
 565          $this->assertEquals($cm1, $cminfo->get_course_module_record(true));
 566          $this->assertEquals($cm2, $cminfo->get_course_module_record(true));
 567  
 568          $cm1 = get_coursemodule_from_id(null, $mods[3]->cmid, 0, true, MUST_EXIST);
 569          $cm2 = get_coursemodule_from_instance('forum', $mods[3]->id, 0, true, MUST_EXIST);
 570          $cminfo = $modinfo->get_cm($mods[3]->cmid);
 571          $record = $DB->get_record('course_modules', array('id' => $mods[3]->cmid));
 572          $this->assertEquals($record, $cminfo->get_course_module_record());
 573          $this->assertEquals($cm1, $cminfo->get_course_module_record(true));
 574          $this->assertEquals($cm2, $cminfo->get_course_module_record(true));
 575  
 576          $cm1 = get_coursemodule_from_id(null, $mods[4]->cmid, 0, true, MUST_EXIST);
 577          $cm2 = get_coursemodule_from_instance('forum', $mods[4]->id, 0, true, MUST_EXIST);
 578          $cminfo = $modinfo->get_cm($mods[4]->cmid);
 579          $record = $DB->get_record('course_modules', array('id' => $mods[4]->cmid));
 580          $this->assertEquals($record, $cminfo->get_course_module_record());
 581          $this->assertEquals($cm1, $cminfo->get_course_module_record(true));
 582          $this->assertEquals($cm2, $cminfo->get_course_module_record(true));
 583  
 584      }
 585  
 586      /**
 587       * Tests the availability property that has been added to course modules
 588       * and sections (just to see that it is correctly saved and accessed).
 589       */
 590      public function test_availability_property() {
 591          global $DB, $CFG;
 592  
 593          $this->resetAfterTest();
 594  
 595          // Create a course with two modules and three sections.
 596          $course = $this->getDataGenerator()->create_course(
 597                  array('format' => 'topics', 'numsections' => 3),
 598                  array('createsections' => true));
 599          $forum = $this->getDataGenerator()->create_module('forum',
 600                  array('course' => $course->id));
 601          $forum2 = $this->getDataGenerator()->create_module('forum',
 602                  array('course' => $course->id));
 603  
 604          // Get modinfo. Check that availability is null for both cm and sections.
 605          $modinfo = get_fast_modinfo($course->id);
 606          $cm = $modinfo->get_cm($forum->cmid);
 607          $this->assertNull($cm->availability);
 608          $section = $modinfo->get_section_info(1, MUST_EXIST);
 609          $this->assertNull($section->availability);
 610  
 611          // Update availability for cm and section in database.
 612          $DB->set_field('course_modules', 'availability', '{}', array('id' => $cm->id));
 613          $DB->set_field('course_sections', 'availability', '{}', array('id' => $section->id));
 614  
 615          // Clear cache and get modinfo again.
 616          rebuild_course_cache($course->id, true);
 617          get_fast_modinfo(0, 0, true);
 618          $modinfo = get_fast_modinfo($course->id);
 619  
 620          // Check values that were changed.
 621          $cm = $modinfo->get_cm($forum->cmid);
 622          $this->assertEquals('{}', $cm->availability);
 623          $section = $modinfo->get_section_info(1, MUST_EXIST);
 624          $this->assertEquals('{}', $section->availability);
 625  
 626          // Check other values are still null.
 627          $cm = $modinfo->get_cm($forum2->cmid);
 628          $this->assertNull($cm->availability);
 629          $section = $modinfo->get_section_info(2, MUST_EXIST);
 630          $this->assertNull($section->availability);
 631      }
 632  
 633      /**
 634       * Tests for get_groups() method.
 635       */
 636      public function test_get_groups() {
 637          $this->resetAfterTest();
 638          $generator = $this->getDataGenerator();
 639  
 640          // Create courses.
 641          $course1 = $generator->create_course();
 642          $course2 = $generator->create_course();
 643          $course3 = $generator->create_course();
 644  
 645          // Create users.
 646          $user1 = $generator->create_user();
 647          $user2 = $generator->create_user();
 648          $user3 = $generator->create_user();
 649  
 650          // Enrol users on courses.
 651          $generator->enrol_user($user1->id, $course1->id);
 652          $generator->enrol_user($user2->id, $course2->id);
 653          $generator->enrol_user($user3->id, $course2->id);
 654          $generator->enrol_user($user3->id, $course3->id);
 655  
 656          // Create groups.
 657          $group1 = $generator->create_group(array('courseid' => $course1->id));
 658          $group2 = $generator->create_group(array('courseid' => $course2->id));
 659          $group3 = $generator->create_group(array('courseid' => $course2->id));
 660  
 661          // Assign users to groups and assert the result.
 662          $this->assertTrue($generator->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id)));
 663          $this->assertTrue($generator->create_group_member(array('groupid' => $group2->id, 'userid' => $user2->id)));
 664          $this->assertTrue($generator->create_group_member(array('groupid' => $group3->id, 'userid' => $user2->id)));
 665          $this->assertTrue($generator->create_group_member(array('groupid' => $group2->id, 'userid' => $user3->id)));
 666  
 667          // Create groupings.
 668          $grouping1 = $generator->create_grouping(array('courseid' => $course1->id));
 669          $grouping2 = $generator->create_grouping(array('courseid' => $course2->id));
 670  
 671          // Assign and assert group to groupings.
 672          groups_assign_grouping($grouping1->id, $group1->id);
 673          groups_assign_grouping($grouping2->id, $group2->id);
 674          groups_assign_grouping($grouping2->id, $group3->id);
 675  
 676          // Test with one single group.
 677          $modinfo = get_fast_modinfo($course1, $user1->id);
 678          $groups = $modinfo->get_groups($grouping1->id);
 679          $this->assertCount(1, $groups);
 680          $this->assertArrayHasKey($group1->id, $groups);
 681  
 682          // Test with two groups.
 683          $modinfo = get_fast_modinfo($course2, $user2->id);
 684          $groups = $modinfo->get_groups();
 685          $this->assertCount(2, $groups);
 686          $this->assertTrue(in_array($group2->id, $groups));
 687          $this->assertTrue(in_array($group3->id, $groups));
 688  
 689          // Test with no groups.
 690          $modinfo = get_fast_modinfo($course3, $user3->id);
 691          $groups = $modinfo->get_groups();
 692          $this->assertCount(0, $groups);
 693          $this->assertArrayNotHasKey($group1->id, $groups);
 694      }
 695  
 696      /**
 697       * Tests the function for constructing a cm_info from mixed data.
 698       */
 699      public function test_create() {
 700          global $CFG, $DB;
 701          $this->resetAfterTest();
 702  
 703          // Create a course and an activity.
 704          $generator = $this->getDataGenerator();
 705          $course = $generator->create_course();
 706          $page = $generator->create_module('page', array('course' => $course->id,
 707                  'name' => 'Annie'));
 708  
 709          // Null is passed through.
 710          $this->assertNull(cm_info::create(null));
 711  
 712          // Stdclass object turns into cm_info.
 713          $cm = cm_info::create(
 714                  (object)array('id' => $page->cmid, 'course' => $course->id));
 715          $this->assertInstanceOf('cm_info', $cm);
 716          $this->assertEquals('Annie', $cm->name);
 717  
 718          // A cm_info object stays as cm_info.
 719          $this->assertSame($cm, cm_info::create($cm));
 720  
 721          // Invalid object (missing fields) causes error.
 722          try {
 723              cm_info::create((object)array('id' => $page->cmid));
 724              $this->fail();
 725          } catch (Exception $e) {
 726              $this->assertInstanceOf('coding_exception', $e);
 727          }
 728  
 729          // Create a second hidden activity.
 730          $hiddenpage = $generator->create_module('page', array('course' => $course->id,
 731                  'name' => 'Annie', 'visible' => 0));
 732  
 733          // Create 2 user accounts, one is a manager who can see everything.
 734          $user = $generator->create_user();
 735          $generator->enrol_user($user->id, $course->id);
 736          $manager = $generator->create_user();
 737          $generator->enrol_user($manager->id, $course->id,
 738                  $DB->get_field('role', 'id', array('shortname' => 'manager'), MUST_EXIST));
 739  
 740          // User can see the normal page but not the hidden one.
 741          $cm = cm_info::create((object)array('id' => $page->cmid, 'course' => $course->id),
 742                  $user->id);
 743          $this->assertTrue($cm->uservisible);
 744          $cm = cm_info::create((object)array('id' => $hiddenpage->cmid, 'course' => $course->id),
 745                  $user->id);
 746          $this->assertFalse($cm->uservisible);
 747  
 748          // Manager can see the hidden one too.
 749          $cm = cm_info::create((object)array('id' => $hiddenpage->cmid, 'course' => $course->id),
 750                  $manager->id);
 751          $this->assertTrue($cm->uservisible);
 752      }
 753  
 754      /**
 755       * Tests function for getting $course and $cm at once quickly from modinfo
 756       * based on cmid or cm record.
 757       */
 758      public function test_get_course_and_cm_from_cmid() {
 759          global $CFG, $DB;
 760          $this->resetAfterTest();
 761  
 762          // Create a course and an activity.
 763          $generator = $this->getDataGenerator();
 764          $course = $generator->create_course(array('shortname' => 'Halls'));
 765          $page = $generator->create_module('page', array('course' => $course->id,
 766                  'name' => 'Annie'));
 767  
 768          // Successful usage.
 769          list($course, $cm) = get_course_and_cm_from_cmid($page->cmid);
 770          $this->assertEquals('Halls', $course->shortname);
 771          $this->assertInstanceOf('cm_info', $cm);
 772          $this->assertEquals('Annie', $cm->name);
 773  
 774          // Specified module type.
 775          list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page');
 776          $this->assertEquals('Annie', $cm->name);
 777  
 778          // With id in object.
 779          $fakecm = (object)array('id' => $page->cmid);
 780          list($course, $cm) = get_course_and_cm_from_cmid($fakecm);
 781          $this->assertEquals('Halls', $course->shortname);
 782          $this->assertEquals('Annie', $cm->name);
 783  
 784          // With both id and course in object.
 785          $fakecm->course = $course->id;
 786          list($course, $cm) = get_course_and_cm_from_cmid($fakecm);
 787          $this->assertEquals('Halls', $course->shortname);
 788          $this->assertEquals('Annie', $cm->name);
 789  
 790          // With supplied course id.
 791          list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', $course->id);
 792          $this->assertEquals('Annie', $cm->name);
 793  
 794          // With supplied course object (modified just so we can check it is
 795          // indeed reusing the supplied object).
 796          $course->silly = true;
 797          list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', $course);
 798          $this->assertEquals('Annie', $cm->name);
 799          $this->assertTrue($course->silly);
 800  
 801          // Incorrect module type.
 802          try {
 803              get_course_and_cm_from_cmid($page->cmid, 'forum');
 804              $this->fail();
 805          } catch (moodle_exception $e) {
 806              $this->assertEquals('invalidcoursemoduleid', $e->errorcode);
 807          }
 808  
 809          // Invalid module name.
 810          try {
 811              get_course_and_cm_from_cmid($page->cmid, 'pigs can fly');
 812              $this->fail();
 813          } catch (coding_exception $e) {
 814              $this->assertStringContainsString('Invalid modulename parameter', $e->getMessage());
 815          }
 816  
 817          // Doesn't exist.
 818          try {
 819              get_course_and_cm_from_cmid($page->cmid + 1);
 820              $this->fail();
 821          } catch (moodle_exception $e) {
 822              $this->assertInstanceOf('dml_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          list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', 0, $user->id);
 838          $this->assertTrue($cm->uservisible);
 839          list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $user->id);
 840          $this->assertFalse($cm->uservisible);
 841  
 842          // Manager can see the hidden one too.
 843          list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $manager->id);
 844          $this->assertTrue($cm->uservisible);
 845      }
 846  
 847      /**
 848       * Tests function for getting $course and $cm at once quickly from modinfo
 849       * based on instance id or record.
 850       */
 851      public function test_get_course_and_cm_from_instance() {
 852          global $CFG, $DB;
 853          $this->resetAfterTest();
 854  
 855          // Create a course and an activity.
 856          $generator = $this->getDataGenerator();
 857          $course = $generator->create_course(array('shortname' => 'Halls'));
 858          $page = $generator->create_module('page', array('course' => $course->id,
 859                  'name' => 'Annie'));
 860  
 861          // Successful usage.
 862          list($course, $cm) = get_course_and_cm_from_instance($page->id, 'page');
 863          $this->assertEquals('Halls', $course->shortname);
 864          $this->assertInstanceOf('cm_info', $cm);
 865          $this->assertEquals('Annie', $cm->name);
 866  
 867          // With id in object.
 868          $fakeinstance = (object)array('id' => $page->id);
 869          list($course, $cm) = get_course_and_cm_from_instance($fakeinstance, 'page');
 870          $this->assertEquals('Halls', $course->shortname);
 871          $this->assertEquals('Annie', $cm->name);
 872  
 873          // With both id and course in object.
 874          $fakeinstance->course = $course->id;
 875          list($course, $cm) = get_course_and_cm_from_instance($fakeinstance, 'page');
 876          $this->assertEquals('Halls', $course->shortname);
 877          $this->assertEquals('Annie', $cm->name);
 878  
 879          // With supplied course id.
 880          list($course, $cm) = get_course_and_cm_from_instance($page->id, 'page', $course->id);
 881          $this->assertEquals('Annie', $cm->name);
 882  
 883          // With supplied course object (modified just so we can check it is
 884          // indeed reusing the supplied object).
 885          $course->silly = true;
 886          list($course, $cm) = get_course_and_cm_from_instance($page->id, 'page', $course);
 887          $this->assertEquals('Annie', $cm->name);
 888          $this->assertTrue($course->silly);
 889  
 890          // Doesn't exist (or is wrong type).
 891          try {
 892              get_course_and_cm_from_instance($page->id, 'forum');
 893              $this->fail();
 894          } catch (moodle_exception $e) {
 895              $this->assertInstanceOf('dml_exception', $e);
 896          }
 897  
 898          // Invalid module name.
 899          try {
 900              get_course_and_cm_from_cmid($page->cmid, '1337 h4x0ring');
 901              $this->fail();
 902          } catch (coding_exception $e) {
 903              $this->assertStringContainsString('Invalid modulename parameter', $e->getMessage());
 904          }
 905  
 906          // Create a second hidden activity.
 907          $hiddenpage = $generator->create_module('page', array('course' => $course->id,
 908                  'name' => 'Annie', 'visible' => 0));
 909  
 910          // Create 2 user accounts, one is a manager who can see everything.
 911          $user = $generator->create_user();
 912          $generator->enrol_user($user->id, $course->id);
 913          $manager = $generator->create_user();
 914          $generator->enrol_user($manager->id, $course->id,
 915                  $DB->get_field('role', 'id', array('shortname' => 'manager'), MUST_EXIST));
 916  
 917          // User can see the normal page but not the hidden one.
 918          list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', 0, $user->id);
 919          $this->assertTrue($cm->uservisible);
 920          list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $user->id);
 921          $this->assertFalse($cm->uservisible);
 922  
 923          // Manager can see the hidden one too.
 924          list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $manager->id);
 925          $this->assertTrue($cm->uservisible);
 926      }
 927  
 928      /**
 929       * Test test_get_section_info_by_id method
 930       *
 931       * @dataProvider get_section_info_by_id_provider
 932       * @covers \course_modinfo::get_section_info_by_id
 933       *
 934       * @param int $sectionnum the section number
 935       * @param int $strictness the search strict mode
 936       * @param bool $expectnull if the function will return a null
 937       * @param bool $expectexception if the function will throw an exception
 938       */
 939      public function test_get_section_info_by_id(
 940          int $sectionnum,
 941          int $strictness = IGNORE_MISSING,
 942          bool $expectnull = false,
 943          bool $expectexception = false
 944      ) {
 945          global $DB;
 946  
 947          $this->resetAfterTest();
 948  
 949          // Create a course with 4 sections.
 950          $course = $this->getDataGenerator()->create_course(['numsections' => 4]);
 951  
 952          // Index sections.
 953          $sectionindex = [];
 954          $modinfo = get_fast_modinfo($course);
 955          $allsections = $modinfo->get_section_info_all();
 956          foreach ($allsections as $section) {
 957              $sectionindex[$section->section] = $section->id;
 958          }
 959  
 960          if ($expectexception) {
 961              $this->expectException(moodle_exception::class);
 962          }
 963  
 964          $sectionid = $sectionindex[$sectionnum] ?? -1;
 965  
 966          $section = $modinfo->get_section_info_by_id($sectionid, $strictness);
 967  
 968          if ($expectnull) {
 969              $this->assertNull($section);
 970          } else {
 971              $this->assertEquals($sectionid, $section->id);
 972              $this->assertEquals($sectionnum, $section->section);
 973          }
 974      }
 975  
 976      /**
 977       * Data provider for test_get_section_info_by_id().
 978       *
 979       * @return array
 980       */
 981      public function get_section_info_by_id_provider() {
 982          return [
 983              'Valid section id' => [
 984                  'sectionnum' => 1,
 985                  'strictness' => IGNORE_MISSING,
 986                  'expectnull' => false,
 987                  'expectexception' => false,
 988              ],
 989              'Section zero' => [
 990                  'sectionnum' => 0,
 991                  'strictness' => IGNORE_MISSING,
 992                  'expectnull' => false,
 993                  'expectexception' => false,
 994              ],
 995              'invalid section ignore missing' => [
 996                  'sectionnum' => -1,
 997                  'strictness' => IGNORE_MISSING,
 998                  'expectnull' => true,
 999                  'expectexception' => false,
1000              ],
1001              'invalid section must exists' => [
1002                  'sectionnum' => -1,
1003                  'strictness' => MUST_EXIST,
1004                  'expectnull' => false,
1005                  'expectexception' => true,
1006              ],
1007          ];
1008      }
1009  
1010      /**
1011       * Test purge_section_cache_by_id method
1012       *
1013       * @covers \course_modinfo::purge_course_section_cache_by_id
1014       * @return void
1015       */
1016      public function test_purge_section_cache_by_id(): void {
1017          $this->resetAfterTest();
1018          $this->setAdminUser();
1019          $cache = cache::make('core', 'coursemodinfo');
1020  
1021          // Generate the course and pre-requisite section.
1022          $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 3], ['createsections' => true]);
1023          // Reset course cache.
1024          rebuild_course_cache($course->id, true);
1025          // Build course cache.
1026          get_fast_modinfo($course->id);
1027          // Get the course modinfo cache.
1028          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1029          // Get the section cache.
1030          $sectioncaches = $coursemodinfo->sectioncache;
1031  
1032          // Make sure that we will have 4 section caches here.
1033          $this->assertCount(4, $sectioncaches);
1034          $this->assertArrayHasKey(0, $sectioncaches);
1035          $this->assertArrayHasKey(1, $sectioncaches);
1036          $this->assertArrayHasKey(2, $sectioncaches);
1037          $this->assertArrayHasKey(3, $sectioncaches);
1038  
1039          // Purge cache for the section by id.
1040          course_modinfo::purge_course_section_cache_by_id($course->id, $sectioncaches[1]->id);
1041          // Get the course modinfo cache.
1042          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1043          // Get the section cache.
1044          $sectioncaches = $coursemodinfo->sectioncache;
1045  
1046          // Make sure that we will have 3 section caches left.
1047          $this->assertCount(3, $sectioncaches);
1048          $this->assertArrayNotHasKey(1, $sectioncaches);
1049          $this->assertArrayHasKey(0, $sectioncaches);
1050          $this->assertArrayHasKey(2, $sectioncaches);
1051          $this->assertArrayHasKey(3, $sectioncaches);
1052          // Make sure that the cacherev will be reset.
1053          $this->assertEquals(-1, $coursemodinfo->cacherev);
1054      }
1055  
1056      /**
1057       * Test purge_section_cache_by_number method
1058       *
1059       * @covers \course_modinfo::purge_course_section_cache_by_number
1060       * @return void
1061       */
1062      public function test_section_cache_by_number(): void {
1063          $this->resetAfterTest();
1064          $this->setAdminUser();
1065          $cache = cache::make('core', 'coursemodinfo');
1066  
1067          // Generate the course and pre-requisite section.
1068          $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 3], ['createsections' => true]);
1069          // Reset course cache.
1070          rebuild_course_cache($course->id, true);
1071          // Build course cache.
1072          get_fast_modinfo($course->id);
1073          // Get the course modinfo cache.
1074          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1075          // Get the section cache.
1076          $sectioncaches = $coursemodinfo->sectioncache;
1077  
1078          // Make sure that we will have 4 section caches here.
1079          $this->assertCount(4, $sectioncaches);
1080          $this->assertArrayHasKey(0, $sectioncaches);
1081          $this->assertArrayHasKey(1, $sectioncaches);
1082          $this->assertArrayHasKey(2, $sectioncaches);
1083          $this->assertArrayHasKey(3, $sectioncaches);
1084  
1085          // Purge cache for the section with section number is 1.
1086          course_modinfo::purge_course_section_cache_by_number($course->id, 1);
1087          // Get the course modinfo cache.
1088          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1089          // Get the section cache.
1090          $sectioncaches = $coursemodinfo->sectioncache;
1091  
1092          // Make sure that we will have 3 section caches left.
1093          $this->assertCount(3, $sectioncaches);
1094          $this->assertArrayNotHasKey(1, $sectioncaches);
1095          $this->assertArrayHasKey(0, $sectioncaches);
1096          $this->assertArrayHasKey(2, $sectioncaches);
1097          $this->assertArrayHasKey(3, $sectioncaches);
1098          // Make sure that the cacherev will be reset.
1099          $this->assertEquals(-1, $coursemodinfo->cacherev);
1100      }
1101  
1102      /**
1103       * Purge a single course module from the cache.
1104       *
1105       * @return void
1106       * @covers \course_modinfo::purge_course_module_cache
1107       */
1108      public function test_purge_course_module(): void {
1109          $this->resetAfterTest();
1110          $this->setAdminUser();
1111          $cache = cache::make('core', 'coursemodinfo');
1112  
1113          // Generate the course and pre-requisite section.
1114          $course = $this->getDataGenerator()->create_course();
1115          $cm1 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
1116          $cm2 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
1117          $cm3 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
1118          $cm4 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
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          $this->assertCount(4, $coursemodinfo->modinfo);
1126          $this->assertArrayHasKey($cm1->cmid, $coursemodinfo->modinfo);
1127          $this->assertArrayHasKey($cm2->cmid, $coursemodinfo->modinfo);
1128          $this->assertArrayHasKey($cm3->cmid, $coursemodinfo->modinfo);
1129          $this->assertArrayHasKey($cm4->cmid, $coursemodinfo->modinfo);
1130  
1131          course_modinfo::purge_course_module_cache($course->id, $cm1->cmid);
1132  
1133          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1134          $this->assertCount(3, $coursemodinfo->modinfo);
1135          $this->assertArrayNotHasKey($cm1->cmid, $coursemodinfo->modinfo);
1136          $this->assertArrayHasKey($cm2->cmid, $coursemodinfo->modinfo);
1137          $this->assertArrayHasKey($cm3->cmid, $coursemodinfo->modinfo);
1138          $this->assertArrayHasKey($cm4->cmid, $coursemodinfo->modinfo);
1139          // Make sure that the cacherev will be reset.
1140          $this->assertEquals(-1, $coursemodinfo->cacherev);
1141      }
1142  
1143      /**
1144       * Purge a multiple course modules from the cache.
1145       *
1146       * @return void
1147       * @covers \course_modinfo::purge_course_modules_cache
1148       */
1149      public function test_purge_multiple_course_modules(): void {
1150          $this->resetAfterTest();
1151          $this->setAdminUser();
1152          $cache = cache::make('core', 'coursemodinfo');
1153  
1154          // Generate the course and pre-requisite section.
1155          $course = $this->getDataGenerator()->create_course();
1156          $cm1 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
1157          $cm2 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
1158          $cm3 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
1159          $cm4 = $this->getDataGenerator()->create_module('page', ['course' => $course]);
1160          // Reset course cache.
1161          rebuild_course_cache($course->id, true);
1162          // Build course cache.
1163          get_fast_modinfo($course->id);
1164          // Get the course modinfo cache.
1165          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1166          $this->assertCount(4, $coursemodinfo->modinfo);
1167          $this->assertArrayHasKey($cm1->cmid, $coursemodinfo->modinfo);
1168          $this->assertArrayHasKey($cm2->cmid, $coursemodinfo->modinfo);
1169          $this->assertArrayHasKey($cm3->cmid, $coursemodinfo->modinfo);
1170          $this->assertArrayHasKey($cm4->cmid, $coursemodinfo->modinfo);
1171  
1172          course_modinfo::purge_course_modules_cache($course->id, [$cm2->cmid, $cm3->cmid]);
1173  
1174          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1175          $this->assertCount(2, $coursemodinfo->modinfo);
1176          $this->assertArrayHasKey($cm1->cmid, $coursemodinfo->modinfo);
1177          $this->assertArrayNotHasKey($cm2->cmid, $coursemodinfo->modinfo);
1178          $this->assertArrayNotHasKey($cm3->cmid, $coursemodinfo->modinfo);
1179          $this->assertArrayHasKey($cm4->cmid, $coursemodinfo->modinfo);
1180          // Make sure that the cacherev will be reset.
1181          $this->assertEquals(-1, $coursemodinfo->cacherev);
1182      }
1183  
1184      /**
1185       * Test get_cm() method to output course module id in the exception text.
1186       *
1187       * @covers \course_modinfo::get_cm
1188       * @return void
1189       */
1190      public function test_invalid_course_module_id(): void {
1191          global $DB;
1192          $this->resetAfterTest();
1193  
1194          $course = $this->getDataGenerator()->create_course();
1195          $forum0 = $this->getDataGenerator()->create_module('assign', ['course' => $course->id], ['section' => 0]);
1196          $forum1 = $this->getDataGenerator()->create_module('assign', ['course' => $course->id], ['section' => 0]);
1197          $forum2 = $this->getDataGenerator()->create_module('assign', ['course' => $course->id], ['section' => 0]);
1198  
1199          // Break section sequence.
1200          $modinfo = get_fast_modinfo($course->id);
1201          $sectionid = $modinfo->get_section_info(0)->id;
1202          $section = $DB->get_record('course_sections', ['id' => $sectionid]);
1203          $sequence = explode(',', $section->sequence);
1204          $sequence = array_diff($sequence, [$forum1->cmid]);
1205          $section->sequence = implode(',', $sequence);
1206          $DB->update_record('course_sections', $section);
1207  
1208          // Assert exception text.
1209          $this->expectException(\moodle_exception::class);
1210          $this->expectExceptionMessage('Invalid course module ID: ' . $forum1->cmid);
1211          delete_course($course, false);
1212      }
1213  
1214      /**
1215       * Tests that if the modinfo cache returns a newer-than-expected version, Moodle won't rebuild
1216       * it.
1217       *
1218       * This is important to avoid wasted time/effort and poor performance, for example in cases
1219       * where multiple requests are accessing the course.
1220       *
1221       * Certain cases could be particularly bad if this test fails. For example, if using clustered
1222       * databases where there is a 100ms delay between updates to the course table being available
1223       * to all users (but no such delay on the cache infrastructure), then during that 100ms, every
1224       * request that calls get_fast_modinfo and uses the read-only database will rebuild the course
1225       * cache. Since these will then create a still-newer version, future requests for the next
1226       * 100ms will also rebuild it again... etc.
1227       *
1228       * @covers \course_modinfo
1229       */
1230      public function test_get_modinfo_with_newer_version(): void {
1231          global $DB;
1232  
1233          $this->resetAfterTest();
1234  
1235          // Get info about a course and build the initial cache, then drop it from memory.
1236          $course = $this->getDataGenerator()->create_course();
1237          get_fast_modinfo($course);
1238          get_fast_modinfo(0, 0, true);
1239  
1240          // User A starts a request, which takes some time...
1241          $useracourse = $DB->get_record('course', ['id' => $course->id]);
1242  
1243          // User B also starts a request and makes a change to the course.
1244          $userbcourse = $DB->get_record('course', ['id' => $course->id]);
1245          $this->getDataGenerator()->create_module('page', ['course' => $course->id]);
1246          rebuild_course_cache($userbcourse->id, false);
1247  
1248          // Finally, user A's request now gets modinfo. It should accept the version from B even
1249          // though the course version (of cache) is newer than the one expected by A.
1250          $before = $DB->perf_get_queries();
1251          $modinfo = get_fast_modinfo($useracourse);
1252          $after = $DB->perf_get_queries();
1253          $this->assertEquals($after, $before, 'Should use cached version, making no DB queries');
1254  
1255          // Obviously, modinfo should include the Page now.
1256          $this->assertCount(1, $modinfo->get_instances_of('page'));
1257      }
1258  }