Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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