Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Unit tests for (some of) mod/quiz/locallib.php.
  19   *
  20   * @package    mod_quiz
  21   * @category   test
  22   * @copyright  2008 Tim Hunt
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  global $CFG;
  30  require_once($CFG->dirroot . '/mod/quiz/locallib.php');
  31  
  32  
  33  /**
  34   * Unit tests for (some of) mod/quiz/locallib.php.
  35   *
  36   * @copyright  2008 Tim Hunt
  37   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class mod_quiz_locallib_testcase extends advanced_testcase {
  40  
  41      public function test_quiz_rescale_grade() {
  42          $quiz = new stdClass();
  43          $quiz->decimalpoints = 2;
  44          $quiz->questiondecimalpoints = 3;
  45          $quiz->grade = 10;
  46          $quiz->sumgrades = 10;
  47          $this->assertEquals(quiz_rescale_grade(0.12345678, $quiz, false), 0.12345678);
  48          $this->assertEquals(quiz_rescale_grade(0.12345678, $quiz, true), format_float(0.12, 2));
  49          $this->assertEquals(quiz_rescale_grade(0.12345678, $quiz, 'question'),
  50              format_float(0.123, 3));
  51          $quiz->sumgrades = 5;
  52          $this->assertEquals(quiz_rescale_grade(0.12345678, $quiz, false), 0.24691356);
  53          $this->assertEquals(quiz_rescale_grade(0.12345678, $quiz, true), format_float(0.25, 2));
  54          $this->assertEquals(quiz_rescale_grade(0.12345678, $quiz, 'question'),
  55              format_float(0.247, 3));
  56      }
  57  
  58      public function quiz_attempt_state_data_provider() {
  59          return [
  60              [quiz_attempt::IN_PROGRESS, null, null, mod_quiz_display_options::DURING],
  61              [quiz_attempt::FINISHED, -90, null, mod_quiz_display_options::IMMEDIATELY_AFTER],
  62              [quiz_attempt::FINISHED, -7200, null, mod_quiz_display_options::LATER_WHILE_OPEN],
  63              [quiz_attempt::FINISHED, -7200, 3600, mod_quiz_display_options::LATER_WHILE_OPEN],
  64              [quiz_attempt::FINISHED, -30, 30, mod_quiz_display_options::IMMEDIATELY_AFTER],
  65              [quiz_attempt::FINISHED, -90, -30, mod_quiz_display_options::AFTER_CLOSE],
  66              [quiz_attempt::FINISHED, -7200, -3600, mod_quiz_display_options::AFTER_CLOSE],
  67              [quiz_attempt::FINISHED, -90, -3600, mod_quiz_display_options::AFTER_CLOSE],
  68              [quiz_attempt::ABANDONED, -10000000, null, mod_quiz_display_options::LATER_WHILE_OPEN],
  69              [quiz_attempt::ABANDONED, -7200, 3600, mod_quiz_display_options::LATER_WHILE_OPEN],
  70              [quiz_attempt::ABANDONED, -7200, -3600, mod_quiz_display_options::AFTER_CLOSE],
  71          ];
  72      }
  73  
  74      /**
  75       * @dataProvider quiz_attempt_state_data_provider
  76       *
  77       * @param unknown $attemptstate as in the quiz_attempts.state DB column.
  78       * @param unknown $relativetimefinish time relative to now when the attempt finished, or null for 0.
  79       * @param unknown $relativetimeclose time relative to now when the quiz closes, or null for 0.
  80       * @param unknown $expectedstate expected result. One of the mod_quiz_display_options constants/
  81       */
  82      public function test_quiz_attempt_state($attemptstate,
  83              $relativetimefinish, $relativetimeclose, $expectedstate) {
  84  
  85          $attempt = new stdClass();
  86          $attempt->state = $attemptstate;
  87          if ($relativetimefinish === null) {
  88              $attempt->timefinish = 0;
  89          } else {
  90              $attempt->timefinish = time() + $relativetimefinish;
  91          }
  92  
  93          $quiz = new stdClass();
  94          if ($relativetimeclose === null) {
  95              $quiz->timeclose = 0;
  96          } else {
  97              $quiz->timeclose = time() + $relativetimeclose;
  98          }
  99  
 100          $this->assertEquals($expectedstate, quiz_attempt_state($quiz, $attempt));
 101      }
 102  
 103      public function test_quiz_question_tostring() {
 104          $question = new stdClass();
 105          $question->qtype = 'multichoice';
 106          $question->name = 'The question name';
 107          $question->questiontext = '<p>What sort of <b>inequality</b> is x &lt; y<img alt="?" src="..."></p>';
 108          $question->questiontextformat = FORMAT_HTML;
 109  
 110          $summary = quiz_question_tostring($question);
 111          $this->assertEquals('<span class="questionname">The question name</span> ' .
 112                  '<span class="questiontext">What sort of INEQUALITY is x &lt; y[?]' . "\n" . '</span>', $summary);
 113      }
 114  
 115      /**
 116       * Test quiz_view
 117       * @return void
 118       */
 119      public function test_quiz_view() {
 120          global $CFG;
 121  
 122          $CFG->enablecompletion = 1;
 123          $this->resetAfterTest();
 124  
 125          $this->setAdminUser();
 126          // Setup test data.
 127          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
 128          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id),
 129                                                              array('completion' => 2, 'completionview' => 1));
 130          $context = context_module::instance($quiz->cmid);
 131          $cm = get_coursemodule_from_instance('quiz', $quiz->id);
 132  
 133          // Trigger and capture the event.
 134          $sink = $this->redirectEvents();
 135  
 136          quiz_view($quiz, $course, $cm, $context);
 137  
 138          $events = $sink->get_events();
 139          // 2 additional events thanks to completion.
 140          $this->assertCount(3, $events);
 141          $event = array_shift($events);
 142  
 143          // Checking that the event contains the expected values.
 144          $this->assertInstanceOf('\mod_quiz\event\course_module_viewed', $event);
 145          $this->assertEquals($context, $event->get_context());
 146          $moodleurl = new \moodle_url('/mod/quiz/view.php', array('id' => $cm->id));
 147          $this->assertEquals($moodleurl, $event->get_url());
 148          $this->assertEventContextNotUsed($event);
 149          $this->assertNotEmpty($event->get_name());
 150          // Check completion status.
 151          $completion = new completion_info($course);
 152          $completiondata = $completion->get_data($cm);
 153          $this->assertEquals(1, $completiondata->completionstate);
 154      }
 155  
 156      /**
 157       * Return false when there are not overrides for this quiz instance.
 158       */
 159      public function test_quiz_is_overriden_calendar_event_no_override() {
 160          global $CFG, $DB;
 161  
 162          $this->resetAfterTest();
 163          $this->setAdminUser();
 164  
 165          $generator = $this->getDataGenerator();
 166          $user = $generator->create_user();
 167          $course = $generator->create_course();
 168          $quizgenerator = $generator->get_plugin_generator('mod_quiz');
 169          $quiz = $quizgenerator->create_instance(['course' => $course->id]);
 170  
 171          $event = new \calendar_event((object)[
 172              'modulename' => 'quiz',
 173              'instance' => $quiz->id,
 174              'userid' => $user->id
 175          ]);
 176  
 177          $this->assertFalse(quiz_is_overriden_calendar_event($event));
 178      }
 179  
 180      /**
 181       * Return false if the given event isn't an quiz module event.
 182       */
 183      public function test_quiz_is_overriden_calendar_event_no_module_event() {
 184          global $CFG, $DB;
 185  
 186          $this->resetAfterTest();
 187          $this->setAdminUser();
 188  
 189          $generator = $this->getDataGenerator();
 190          $user = $generator->create_user();
 191          $course = $generator->create_course();
 192          $quizgenerator = $generator->get_plugin_generator('mod_quiz');
 193          $quiz = $quizgenerator->create_instance(['course' => $course->id]);
 194  
 195          $event = new \calendar_event((object)[
 196              'userid' => $user->id
 197          ]);
 198  
 199          $this->assertFalse(quiz_is_overriden_calendar_event($event));
 200      }
 201  
 202      /**
 203       * Return false if there is overrides for this use but they belong to another quiz
 204       * instance.
 205       */
 206      public function test_quiz_is_overriden_calendar_event_different_quiz_instance() {
 207          global $CFG, $DB;
 208  
 209          $this->resetAfterTest();
 210          $this->setAdminUser();
 211  
 212          $generator = $this->getDataGenerator();
 213          $user = $generator->create_user();
 214          $course = $generator->create_course();
 215          $quizgenerator = $generator->get_plugin_generator('mod_quiz');
 216          $quiz = $quizgenerator->create_instance(['course' => $course->id]);
 217          $quiz2 = $quizgenerator->create_instance(['course' => $course->id]);
 218  
 219          $event = new \calendar_event((object) [
 220              'modulename' => 'quiz',
 221              'instance' => $quiz->id,
 222              'userid' => $user->id
 223          ]);
 224  
 225          $record = (object) [
 226              'quiz' => $quiz2->id,
 227              'userid' => $user->id
 228          ];
 229  
 230          $DB->insert_record('quiz_overrides', $record);
 231  
 232          $this->assertFalse(quiz_is_overriden_calendar_event($event));
 233      }
 234  
 235      /**
 236       * Return true if there is a user override for this event and quiz instance.
 237       */
 238      public function test_quiz_is_overriden_calendar_event_user_override() {
 239          global $CFG, $DB;
 240  
 241          $this->resetAfterTest();
 242          $this->setAdminUser();
 243  
 244          $generator = $this->getDataGenerator();
 245          $user = $generator->create_user();
 246          $course = $generator->create_course();
 247          $quizgenerator = $generator->get_plugin_generator('mod_quiz');
 248          $quiz = $quizgenerator->create_instance(['course' => $course->id]);
 249  
 250          $event = new \calendar_event((object) [
 251              'modulename' => 'quiz',
 252              'instance' => $quiz->id,
 253              'userid' => $user->id
 254          ]);
 255  
 256          $record = (object) [
 257              'quiz' => $quiz->id,
 258              'userid' => $user->id
 259          ];
 260  
 261          $DB->insert_record('quiz_overrides', $record);
 262  
 263          $this->assertTrue(quiz_is_overriden_calendar_event($event));
 264      }
 265  
 266      /**
 267       * Return true if there is a group override for the event and quiz instance.
 268       */
 269      public function test_quiz_is_overriden_calendar_event_group_override() {
 270          global $CFG, $DB;
 271  
 272          $this->resetAfterTest();
 273          $this->setAdminUser();
 274  
 275          $generator = $this->getDataGenerator();
 276          $user = $generator->create_user();
 277          $course = $generator->create_course();
 278          $quizgenerator = $generator->get_plugin_generator('mod_quiz');
 279          $quiz = $quizgenerator->create_instance(['course' => $course->id]);
 280          $group = $this->getDataGenerator()->create_group(array('courseid' => $quiz->course));
 281          $groupid = $group->id;
 282          $userid = $user->id;
 283  
 284          $event = new \calendar_event((object) [
 285              'modulename' => 'quiz',
 286              'instance' => $quiz->id,
 287              'groupid' => $groupid
 288          ]);
 289  
 290          $record = (object) [
 291              'quiz' => $quiz->id,
 292              'groupid' => $groupid
 293          ];
 294  
 295          $DB->insert_record('quiz_overrides', $record);
 296  
 297          $this->assertTrue(quiz_is_overriden_calendar_event($event));
 298      }
 299  
 300      /**
 301       * Test test_quiz_get_user_timeclose().
 302       */
 303      public function test_quiz_get_user_timeclose() {
 304          global $DB;
 305  
 306          $this->resetAfterTest();
 307          $this->setAdminUser();
 308  
 309          $basetimestamp = time(); // The timestamp we will base the enddates on.
 310  
 311          // Create generator, course and quizzes.
 312          $student1 = $this->getDataGenerator()->create_user();
 313          $student2 = $this->getDataGenerator()->create_user();
 314          $student3 = $this->getDataGenerator()->create_user();
 315          $teacher = $this->getDataGenerator()->create_user();
 316          $course = $this->getDataGenerator()->create_course();
 317          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 318  
 319          // Both quizzes close in two hours.
 320          $quiz1 = $quizgenerator->create_instance(array('course' => $course->id, 'timeclose' => $basetimestamp + 7200));
 321          $quiz2 = $quizgenerator->create_instance(array('course' => $course->id, 'timeclose' => $basetimestamp + 7200));
 322          $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
 323          $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
 324  
 325          $student1id = $student1->id;
 326          $student2id = $student2->id;
 327          $student3id = $student3->id;
 328          $teacherid = $teacher->id;
 329  
 330          // Users enrolments.
 331          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 332          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
 333          $this->getDataGenerator()->enrol_user($student1id, $course->id, $studentrole->id, 'manual');
 334          $this->getDataGenerator()->enrol_user($student2id, $course->id, $studentrole->id, 'manual');
 335          $this->getDataGenerator()->enrol_user($student3id, $course->id, $studentrole->id, 'manual');
 336          $this->getDataGenerator()->enrol_user($teacherid, $course->id, $teacherrole->id, 'manual');
 337  
 338          // Create groups.
 339          $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
 340          $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
 341          $group1id = $group1->id;
 342          $group2id = $group2->id;
 343          $this->getDataGenerator()->create_group_member(array('userid' => $student1id, 'groupid' => $group1id));
 344          $this->getDataGenerator()->create_group_member(array('userid' => $student2id, 'groupid' => $group2id));
 345  
 346          // Group 1 gets an group override for quiz 1 to close in three hours.
 347          $record1 = (object) [
 348              'quiz' => $quiz1->id,
 349              'groupid' => $group1id,
 350              'timeclose' => $basetimestamp + 10800 // In three hours.
 351          ];
 352          $DB->insert_record('quiz_overrides', $record1);
 353  
 354          // Let's test quiz 1 closes in three hours for user student 1 since member of group 1.
 355          // Quiz 2 closes in two hours.
 356          $this->setUser($student1id);
 357          $params = new stdClass();
 358  
 359          $comparearray = array();
 360          $object = new stdClass();
 361          $object->id = $quiz1->id;
 362          $object->usertimeclose = $basetimestamp + 10800; // The overriden timeclose for quiz 1.
 363  
 364          $comparearray[$quiz1->id] = $object;
 365  
 366          $object = new stdClass();
 367          $object->id = $quiz2->id;
 368          $object->usertimeclose = $basetimestamp + 7200; // The unchanged timeclose for quiz 2.
 369  
 370          $comparearray[$quiz2->id] = $object;
 371  
 372          $this->assertEquals($comparearray, quiz_get_user_timeclose($course->id));
 373  
 374          // Let's test quiz 1 closes in two hours (the original value) for user student 3 since member of no group.
 375          $this->setUser($student3id);
 376          $params = new stdClass();
 377  
 378          $comparearray = array();
 379          $object = new stdClass();
 380          $object->id = $quiz1->id;
 381          $object->usertimeclose = $basetimestamp + 7200; // The original timeclose for quiz 1.
 382  
 383          $comparearray[$quiz1->id] = $object;
 384  
 385          $object = new stdClass();
 386          $object->id = $quiz2->id;
 387          $object->usertimeclose = $basetimestamp + 7200; // The original timeclose for quiz 2.
 388  
 389          $comparearray[$quiz2->id] = $object;
 390  
 391          $this->assertEquals($comparearray, quiz_get_user_timeclose($course->id));
 392  
 393          // User 2 gets an user override for quiz 1 to close in four hours.
 394          $record2 = (object) [
 395              'quiz' => $quiz1->id,
 396              'userid' => $student2id,
 397              'timeclose' => $basetimestamp + 14400 // In four hours.
 398          ];
 399          $DB->insert_record('quiz_overrides', $record2);
 400  
 401          // Let's test quiz 1 closes in four hours for user student 2 since personally overriden.
 402          // Quiz 2 closes in two hours.
 403          $this->setUser($student2id);
 404  
 405          $comparearray = array();
 406          $object = new stdClass();
 407          $object->id = $quiz1->id;
 408          $object->usertimeclose = $basetimestamp + 14400; // The overriden timeclose for quiz 1.
 409  
 410          $comparearray[$quiz1->id] = $object;
 411  
 412          $object = new stdClass();
 413          $object->id = $quiz2->id;
 414          $object->usertimeclose = $basetimestamp + 7200; // The unchanged timeclose for quiz 2.
 415  
 416          $comparearray[$quiz2->id] = $object;
 417  
 418          $this->assertEquals($comparearray, quiz_get_user_timeclose($course->id));
 419  
 420          // Let's test a teacher sees the original times.
 421          // Quiz 1 and quiz 2 close in two hours.
 422          $this->setUser($teacherid);
 423  
 424          $comparearray = array();
 425          $object = new stdClass();
 426          $object->id = $quiz1->id;
 427          $object->usertimeclose = $basetimestamp + 7200; // The unchanged timeclose for quiz 1.
 428  
 429          $comparearray[$quiz1->id] = $object;
 430  
 431          $object = new stdClass();
 432          $object->id = $quiz2->id;
 433          $object->usertimeclose = $basetimestamp + 7200; // The unchanged timeclose for quiz 2.
 434  
 435          $comparearray[$quiz2->id] = $object;
 436  
 437          $this->assertEquals($comparearray, quiz_get_user_timeclose($course->id));
 438      }
 439  
 440      /**
 441       * This function creates a quiz with some standard (non-random) and some random questions.
 442       * The standard questions are created first and then random questions follow them.
 443       * So in a quiz with 3 standard question and 2 random question, the first random question is at slot 4.
 444       *
 445       * @param int $qnum Number of standard questions that should be created in the quiz.
 446       * @param int $randomqnum Number of random questions that should be created in the quiz.
 447       * @param array $questiontags Tags to be used for random questions.
 448       *      This is an array in the following format:
 449       *      [
 450       *          0 => ['foo', 'bar'],
 451       *          1 => ['baz', 'qux']
 452       *      ]
 453       * @param string[] $unusedtags Some additional tags to be created.
 454       * @return array An array of 2 elements: $quiz and $tagobjects.
 455       *      $tagobjects is an associative array of all created tag objects with its key being tag names.
 456       */
 457      private function setup_quiz_and_tags($qnum, $randomqnum, $questiontags = [], $unusedtags = []) {
 458          global $SITE;
 459  
 460          $tagobjects = [];
 461  
 462          // Get all the tags that need to be created.
 463          $alltags = [];
 464          foreach ($questiontags as $questiontag) {
 465              $alltags = array_merge($alltags, $questiontag);
 466          }
 467          $alltags = array_merge($alltags, $unusedtags);
 468          $alltags = array_unique($alltags);
 469  
 470          // Create tags.
 471          foreach ($alltags as $tagname) {
 472              $tagrecord = array(
 473                  'isstandard' => 1,
 474                  'flag' => 0,
 475                  'rawname' => $tagname,
 476                  'description' => $tagname . ' desc'
 477              );
 478              $tagobjects[$tagname] = $this->getDataGenerator()->create_tag($tagrecord);
 479          }
 480  
 481          // Create a quiz.
 482          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 483          $quiz = $quizgenerator->create_instance(array('course' => $SITE->id, 'questionsperpage' => 3, 'grade' => 100.0));
 484  
 485          // Create a question category in the system context.
 486          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 487          $cat = $questiongenerator->create_question_category();
 488  
 489          // Setup standard questions.
 490          for ($i = 0; $i < $qnum; $i++) {
 491              $question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 492              quiz_add_quiz_question($question->id, $quiz);
 493          }
 494          // Setup random questions.
 495          for ($i = 0; $i < $randomqnum; $i++) {
 496              // Just create a standard question first, so there would be enough questions to pick a random question from.
 497              $question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 498              $tagids = [];
 499              if (!empty($questiontags[$i])) {
 500                  foreach ($questiontags[$i] as $tagname) {
 501                      $tagids[] = $tagobjects[$tagname]->id;
 502                  }
 503              }
 504              quiz_add_random_questions($quiz, 0, $cat->id, 1, false, $tagids);
 505          }
 506  
 507          return array($quiz, $tagobjects);
 508      }
 509  
 510      public function test_quiz_retrieve_slot_tags() {
 511          global $DB;
 512  
 513          $this->resetAfterTest();
 514          $this->setAdminUser();
 515  
 516          list($quiz, $tags) = $this->setup_quiz_and_tags(1, 1, [['foo', 'bar']], ['baz']);
 517  
 518          // Get the random question's slotid. It is at the second slot.
 519          $slotid = $DB->get_field('quiz_slots', 'id', array('quizid' => $quiz->id, 'slot' => 2));
 520          $slottags = quiz_retrieve_slot_tags($slotid);
 521  
 522          $this->assertEqualsCanonicalizing(
 523                  [
 524                      ['tagid' => $tags['foo']->id, 'tagname' => $tags['foo']->name],
 525                      ['tagid' => $tags['bar']->id, 'tagname' => $tags['bar']->name]
 526                  ],
 527                  array_map(function($slottag) {
 528                      return ['tagid' => $slottag->tagid, 'tagname' => $slottag->tagname];
 529                  }, $slottags));
 530      }
 531  
 532      public function test_quiz_retrieve_slot_tags_with_removed_tag() {
 533          global $DB;
 534  
 535          $this->resetAfterTest();
 536          $this->setAdminUser();
 537  
 538          list($quiz, $tags) = $this->setup_quiz_and_tags(1, 1, [['foo', 'bar']], ['baz']);
 539  
 540          // Get the random question's slotid. It is at the second slot.
 541          $slotid = $DB->get_field('quiz_slots', 'id', array('quizid' => $quiz->id, 'slot' => 2));
 542          $slottags = quiz_retrieve_slot_tags($slotid);
 543  
 544          // Now remove the foo tag and check again.
 545          core_tag_tag::delete_tags([$tags['foo']->id]);
 546          $slottags = quiz_retrieve_slot_tags($slotid);
 547  
 548          $this->assertEqualsCanonicalizing(
 549                  [
 550                      ['tagid' => null, 'tagname' => $tags['foo']->name],
 551                      ['tagid' => $tags['bar']->id, 'tagname' => $tags['bar']->name]
 552                  ],
 553                  array_map(function($slottag) {
 554                      return ['tagid' => $slottag->tagid, 'tagname' => $slottag->tagname];
 555                  }, $slottags));
 556      }
 557  
 558      public function test_quiz_retrieve_slot_tags_for_standard_question() {
 559          global $DB;
 560  
 561          $this->resetAfterTest();
 562          $this->setAdminUser();
 563  
 564          list($quiz, $tags) = $this->setup_quiz_and_tags(1, 1, [['foo', 'bar']]);
 565  
 566          // Get the standard question's slotid. It is at the first slot.
 567          $slotid = $DB->get_field('quiz_slots', 'id', array('quizid' => $quiz->id, 'slot' => 1));
 568  
 569          // There should be no slot tags for a non-random question.
 570          $this->assertCount(0, quiz_retrieve_slot_tags($slotid));
 571      }
 572  
 573      public function test_quiz_retrieve_slot_tag_ids() {
 574          global $DB;
 575  
 576          $this->resetAfterTest();
 577          $this->setAdminUser();
 578  
 579          list($quiz, $tags) = $this->setup_quiz_and_tags(1, 1, [['foo', 'bar']], ['baz']);
 580  
 581          // Get the random question's slotid. It is at the second slot.
 582          $slotid = $DB->get_field('quiz_slots', 'id', array('quizid' => $quiz->id, 'slot' => 2));
 583          $tagids = quiz_retrieve_slot_tag_ids($slotid);
 584  
 585          $this->assertEqualsCanonicalizing([$tags['foo']->id, $tags['bar']->id], $tagids);
 586      }
 587  
 588      public function test_quiz_retrieve_slot_tag_ids_for_standard_question() {
 589          global $DB;
 590  
 591          $this->resetAfterTest();
 592          $this->setAdminUser();
 593  
 594          list($quiz, $tags) = $this->setup_quiz_and_tags(1, 1, [['foo', 'bar']], ['baz']);
 595  
 596          // Get the standard question's slotid. It is at the first slot.
 597          $slotid = $DB->get_field('quiz_slots', 'id', array('quizid' => $quiz->id, 'slot' => 1));
 598          $tagids = quiz_retrieve_slot_tag_ids($slotid);
 599  
 600          $this->assertEqualsCanonicalizing([], $tagids);
 601      }
 602  
 603      /**
 604       * Data provider for the get_random_question_summaries test.
 605       */
 606      public function get_quiz_retrieve_tags_for_slot_ids_test_cases() {
 607          return [
 608              'no questions' => [
 609                  'questioncount' => 0,
 610                  'randomquestioncount' => 0,
 611                  'randomquestiontags' => [],
 612                  'unusedtags' => [],
 613                  'removeslottagids' => [],
 614                  'expected' => []
 615              ],
 616              'only regular questions' => [
 617                  'questioncount' => 2,
 618                  'randomquestioncount' => 0,
 619                  'randomquestiontags' => [],
 620                  'unusedtags' => ['unused1', 'unused2'],
 621                  'removeslottagids' => [],
 622                  'expected' => [
 623                      1 => [],
 624                      2 => []
 625                  ]
 626              ],
 627              'only random questions 1' => [
 628                  'questioncount' => 0,
 629                  'randomquestioncount' => 2,
 630                  'randomquestiontags' => [
 631                      0 => ['foo'],
 632                      1 => []
 633                  ],
 634                  'unusedtags' => ['unused1', 'unused2'],
 635                  'removeslottagids' => [],
 636                  'expected' => [
 637                      1 => ['foo'],
 638                      2 => []
 639                  ]
 640              ],
 641              'only random questions 2' => [
 642                  'questioncount' => 0,
 643                  'randomquestioncount' => 2,
 644                  'randomquestiontags' => [
 645                      0 => ['foo', 'bop'],
 646                      1 => ['bar']
 647                  ],
 648                  'unusedtags' => ['unused1', 'unused2'],
 649                  'removeslottagids' => [],
 650                  'expected' => [
 651                      1 => ['foo', 'bop'],
 652                      2 => ['bar']
 653                  ]
 654              ],
 655              'only random questions 3' => [
 656                  'questioncount' => 0,
 657                  'randomquestioncount' => 2,
 658                  'randomquestiontags' => [
 659                      0 => ['foo', 'bop'],
 660                      1 => ['bar', 'foo']
 661                  ],
 662                  'unusedtags' => ['unused1', 'unused2'],
 663                  'removeslottagids' => [],
 664                  'expected' => [
 665                      1 => ['foo', 'bop'],
 666                      2 => ['bar', 'foo']
 667                  ]
 668              ],
 669              'combination of questions 1' => [
 670                  'questioncount' => 2,
 671                  'randomquestioncount' => 2,
 672                  'randomquestiontags' => [
 673                      0 => ['foo'],
 674                      1 => []
 675                  ],
 676                  'unusedtags' => ['unused1', 'unused2'],
 677                  'removeslottagids' => [],
 678                  'expected' => [
 679                      1 => [],
 680                      2 => [],
 681                      3 => ['foo'],
 682                      4 => []
 683                  ]
 684              ],
 685              'combination of questions 2' => [
 686                  'questioncount' => 2,
 687                  'randomquestioncount' => 2,
 688                  'randomquestiontags' => [
 689                      0 => ['foo', 'bop'],
 690                      1 => ['bar']
 691                  ],
 692                  'unusedtags' => ['unused1', 'unused2'],
 693                  'removeslottagids' => [],
 694                  'expected' => [
 695                      1 => [],
 696                      2 => [],
 697                      3 => ['foo', 'bop'],
 698                      4 => ['bar']
 699                  ]
 700              ],
 701              'combination of questions 3' => [
 702                  'questioncount' => 2,
 703                  'randomquestioncount' => 2,
 704                  'randomquestiontags' => [
 705                      0 => ['foo', 'bop'],
 706                      1 => ['bar', 'foo']
 707                  ],
 708                  'unusedtags' => ['unused1', 'unused2'],
 709                  'removeslottagids' => [],
 710                  'expected' => [
 711                      1 => [],
 712                      2 => [],
 713                      3 => ['foo', 'bop'],
 714                      4 => ['bar', 'foo']
 715                  ]
 716              ],
 717              'load from name 1' => [
 718                  'questioncount' => 2,
 719                  'randomquestioncount' => 2,
 720                  'randomquestiontags' => [
 721                      0 => ['foo'],
 722                      1 => []
 723                  ],
 724                  'unusedtags' => ['unused1', 'unused2'],
 725                  'removeslottagids' => [3],
 726                  'expected' => [
 727                      1 => [],
 728                      2 => [],
 729                      3 => ['foo'],
 730                      4 => []
 731                  ]
 732              ],
 733              'load from name 2' => [
 734                  'questioncount' => 2,
 735                  'randomquestioncount' => 2,
 736                  'randomquestiontags' => [
 737                      0 => ['foo', 'bop'],
 738                      1 => ['bar']
 739                  ],
 740                  'unusedtags' => ['unused1', 'unused2'],
 741                  'removeslottagids' => [3],
 742                  'expected' => [
 743                      1 => [],
 744                      2 => [],
 745                      3 => ['foo', 'bop'],
 746                      4 => ['bar']
 747                  ]
 748              ],
 749              'load from name 3' => [
 750                  'questioncount' => 2,
 751                  'randomquestioncount' => 2,
 752                  'randomquestiontags' => [
 753                      0 => ['foo', 'bop'],
 754                      1 => ['bar', 'foo']
 755                  ],
 756                  'unusedtags' => ['unused1', 'unused2'],
 757                  'removeslottagids' => [3],
 758                  'expected' => [
 759                      1 => [],
 760                      2 => [],
 761                      3 => ['foo', 'bop'],
 762                      4 => ['bar', 'foo']
 763                  ]
 764              ]
 765          ];
 766      }
 767  
 768      /**
 769       * Test the quiz_retrieve_tags_for_slot_ids function with various parameter
 770       * combinations.
 771       *
 772       * @dataProvider get_quiz_retrieve_tags_for_slot_ids_test_cases()
 773       * @param int $questioncount The number of regular questions to create
 774       * @param int $randomquestioncount The number of random questions to create
 775       * @param array $randomquestiontags The tags for the random questions
 776       * @param string[] $unusedtags Additional tags to create to populate the DB with data
 777       * @param int[] $removeslottagids Slot numbers to remove tag ids for
 778       * @param array $expected The expected output of tag names indexed by slot number
 779       */
 780      public function test_quiz_retrieve_tags_for_slot_ids_combinations(
 781          $questioncount,
 782          $randomquestioncount,
 783          $randomquestiontags,
 784          $unusedtags,
 785          $removeslottagids,
 786          $expected
 787      ) {
 788          global $DB;
 789  
 790          $this->resetAfterTest();
 791          $this->setAdminUser();
 792  
 793          list($quiz, $tags) = $this->setup_quiz_and_tags(
 794              $questioncount,
 795              $randomquestioncount,
 796              $randomquestiontags,
 797              $unusedtags
 798          );
 799  
 800          $slots = $DB->get_records('quiz_slots', ['quizid' => $quiz->id]);
 801          $slotids = [];
 802          $slotsbynumber = [];
 803          foreach ($slots as $slot) {
 804              $slotids[] = $slot->id;
 805              $slotsbynumber[$slot->slot] = $slot;
 806          }
 807  
 808          if (!empty($removeslottagids)) {
 809              // The slots to remove are the slot numbers not the slot id so we need
 810              // to get the ids for the DB call.
 811              $idstonull = array_map(function($slot) use ($slotsbynumber) {
 812                  return $slotsbynumber[$slot]->id;
 813              }, $removeslottagids);
 814              list($sql, $params) = $DB->get_in_or_equal($idstonull);
 815              // Null out the tagid column to force the code to look up the tag by name.
 816              $DB->set_field_select('quiz_slot_tags', 'tagid', null, "slotid {$sql}", $params);
 817          }
 818  
 819          $slottagsbyslotids = quiz_retrieve_tags_for_slot_ids($slotids);
 820          // Convert the result into an associative array of slotid => [... tag names..]
 821          // to make it easier to compare.
 822          $actual = array_map(function($slottags) {
 823              $names = array_map(function($slottag) {
 824                  return $slottag->tagname;
 825              }, $slottags);
 826              // Make sure the names are sorted for comparison.
 827              sort($names);
 828              return $names;
 829          }, $slottagsbyslotids);
 830  
 831          $formattedexptected = [];
 832          // The expected values are indexed by slot number rather than id so let
 833          // convert it to use the id so that we can compare the results.
 834          foreach ($expected as $slot => $tagnames) {
 835              sort($tagnames);
 836              $slotid = $slotsbynumber[$slot]->id;
 837              $formattedexptected[$slotid] = $tagnames;
 838          }
 839  
 840          $this->assertEquals($formattedexptected, $actual);
 841      }
 842  }