Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 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 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * 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->assertEquals(
 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                  '', 0.0, 10, true);
 531      }
 532  
 533      public function test_quiz_retrieve_slot_tags_with_removed_tag() {
 534          global $DB;
 535  
 536          $this->resetAfterTest();
 537          $this->setAdminUser();
 538  
 539          list($quiz, $tags) = $this->setup_quiz_and_tags(1, 1, [['foo', 'bar']], ['baz']);
 540  
 541          // Get the random question's slotid. It is at the second slot.
 542          $slotid = $DB->get_field('quiz_slots', 'id', array('quizid' => $quiz->id, 'slot' => 2));
 543          $slottags = quiz_retrieve_slot_tags($slotid);
 544  
 545          // Now remove the foo tag and check again.
 546          core_tag_tag::delete_tags([$tags['foo']->id]);
 547          $slottags = quiz_retrieve_slot_tags($slotid);
 548  
 549          $this->assertEquals(
 550                  [
 551                      ['tagid' => null, 'tagname' => $tags['foo']->name],
 552                      ['tagid' => $tags['bar']->id, 'tagname' => $tags['bar']->name]
 553                  ],
 554                  array_map(function($slottag) {
 555                      return ['tagid' => $slottag->tagid, 'tagname' => $slottag->tagname];
 556                  }, $slottags),
 557                  '', 0.0, 10, true);
 558      }
 559  
 560      public function test_quiz_retrieve_slot_tags_for_standard_question() {
 561          global $DB;
 562  
 563          $this->resetAfterTest();
 564          $this->setAdminUser();
 565  
 566          list($quiz, $tags) = $this->setup_quiz_and_tags(1, 1, [['foo', 'bar']]);
 567  
 568          // Get the standard question's slotid. It is at the first slot.
 569          $slotid = $DB->get_field('quiz_slots', 'id', array('quizid' => $quiz->id, 'slot' => 1));
 570  
 571          // There should be no slot tags for a non-random question.
 572          $this->assertCount(0, quiz_retrieve_slot_tags($slotid));
 573      }
 574  
 575      public function test_quiz_retrieve_slot_tag_ids() {
 576          global $DB;
 577  
 578          $this->resetAfterTest();
 579          $this->setAdminUser();
 580  
 581          list($quiz, $tags) = $this->setup_quiz_and_tags(1, 1, [['foo', 'bar']], ['baz']);
 582  
 583          // Get the random question's slotid. It is at the second slot.
 584          $slotid = $DB->get_field('quiz_slots', 'id', array('quizid' => $quiz->id, 'slot' => 2));
 585          $tagids = quiz_retrieve_slot_tag_ids($slotid);
 586  
 587          $this->assertEquals([$tags['foo']->id, $tags['bar']->id], $tagids, '', 0.0, 10, true);
 588      }
 589  
 590      public function test_quiz_retrieve_slot_tag_ids_for_standard_question() {
 591          global $DB;
 592  
 593          $this->resetAfterTest();
 594          $this->setAdminUser();
 595  
 596          list($quiz, $tags) = $this->setup_quiz_and_tags(1, 1, [['foo', 'bar']], ['baz']);
 597  
 598          // Get the standard question's slotid. It is at the first slot.
 599          $slotid = $DB->get_field('quiz_slots', 'id', array('quizid' => $quiz->id, 'slot' => 1));
 600          $tagids = quiz_retrieve_slot_tag_ids($slotid);
 601  
 602          $this->assertEquals([], $tagids, '', 0.0, 10, true);
 603      }
 604  
 605      /**
 606       * Data provider for the get_random_question_summaries test.
 607       */
 608      public function get_quiz_retrieve_tags_for_slot_ids_test_cases() {
 609          return [
 610              'no questions' => [
 611                  'questioncount' => 0,
 612                  'randomquestioncount' => 0,
 613                  'randomquestiontags' => [],
 614                  'unusedtags' => [],
 615                  'removeslottagids' => [],
 616                  'expected' => []
 617              ],
 618              'only regular questions' => [
 619                  'questioncount' => 2,
 620                  'randomquestioncount' => 0,
 621                  'randomquestiontags' => [],
 622                  'unusedtags' => ['unused1', 'unused2'],
 623                  'removeslottagids' => [],
 624                  'expected' => [
 625                      1 => [],
 626                      2 => []
 627                  ]
 628              ],
 629              'only random questions 1' => [
 630                  'questioncount' => 0,
 631                  'randomquestioncount' => 2,
 632                  'randomquestiontags' => [
 633                      0 => ['foo'],
 634                      1 => []
 635                  ],
 636                  'unusedtags' => ['unused1', 'unused2'],
 637                  'removeslottagids' => [],
 638                  'expected' => [
 639                      1 => ['foo'],
 640                      2 => []
 641                  ]
 642              ],
 643              'only random questions 2' => [
 644                  'questioncount' => 0,
 645                  'randomquestioncount' => 2,
 646                  'randomquestiontags' => [
 647                      0 => ['foo', 'bop'],
 648                      1 => ['bar']
 649                  ],
 650                  'unusedtags' => ['unused1', 'unused2'],
 651                  'removeslottagids' => [],
 652                  'expected' => [
 653                      1 => ['foo', 'bop'],
 654                      2 => ['bar']
 655                  ]
 656              ],
 657              'only random questions 3' => [
 658                  'questioncount' => 0,
 659                  'randomquestioncount' => 2,
 660                  'randomquestiontags' => [
 661                      0 => ['foo', 'bop'],
 662                      1 => ['bar', 'foo']
 663                  ],
 664                  'unusedtags' => ['unused1', 'unused2'],
 665                  'removeslottagids' => [],
 666                  'expected' => [
 667                      1 => ['foo', 'bop'],
 668                      2 => ['bar', 'foo']
 669                  ]
 670              ],
 671              'combination of questions 1' => [
 672                  'questioncount' => 2,
 673                  'randomquestioncount' => 2,
 674                  'randomquestiontags' => [
 675                      0 => ['foo'],
 676                      1 => []
 677                  ],
 678                  'unusedtags' => ['unused1', 'unused2'],
 679                  'removeslottagids' => [],
 680                  'expected' => [
 681                      1 => [],
 682                      2 => [],
 683                      3 => ['foo'],
 684                      4 => []
 685                  ]
 686              ],
 687              'combination of questions 2' => [
 688                  'questioncount' => 2,
 689                  'randomquestioncount' => 2,
 690                  'randomquestiontags' => [
 691                      0 => ['foo', 'bop'],
 692                      1 => ['bar']
 693                  ],
 694                  'unusedtags' => ['unused1', 'unused2'],
 695                  'removeslottagids' => [],
 696                  'expected' => [
 697                      1 => [],
 698                      2 => [],
 699                      3 => ['foo', 'bop'],
 700                      4 => ['bar']
 701                  ]
 702              ],
 703              'combination of questions 3' => [
 704                  'questioncount' => 2,
 705                  'randomquestioncount' => 2,
 706                  'randomquestiontags' => [
 707                      0 => ['foo', 'bop'],
 708                      1 => ['bar', 'foo']
 709                  ],
 710                  'unusedtags' => ['unused1', 'unused2'],
 711                  'removeslottagids' => [],
 712                  'expected' => [
 713                      1 => [],
 714                      2 => [],
 715                      3 => ['foo', 'bop'],
 716                      4 => ['bar', 'foo']
 717                  ]
 718              ],
 719              'load from name 1' => [
 720                  'questioncount' => 2,
 721                  'randomquestioncount' => 2,
 722                  'randomquestiontags' => [
 723                      0 => ['foo'],
 724                      1 => []
 725                  ],
 726                  'unusedtags' => ['unused1', 'unused2'],
 727                  'removeslottagids' => [3],
 728                  'expected' => [
 729                      1 => [],
 730                      2 => [],
 731                      3 => ['foo'],
 732                      4 => []
 733                  ]
 734              ],
 735              'load from name 2' => [
 736                  'questioncount' => 2,
 737                  'randomquestioncount' => 2,
 738                  'randomquestiontags' => [
 739                      0 => ['foo', 'bop'],
 740                      1 => ['bar']
 741                  ],
 742                  'unusedtags' => ['unused1', 'unused2'],
 743                  'removeslottagids' => [3],
 744                  'expected' => [
 745                      1 => [],
 746                      2 => [],
 747                      3 => ['foo', 'bop'],
 748                      4 => ['bar']
 749                  ]
 750              ],
 751              'load from name 3' => [
 752                  'questioncount' => 2,
 753                  'randomquestioncount' => 2,
 754                  'randomquestiontags' => [
 755                      0 => ['foo', 'bop'],
 756                      1 => ['bar', 'foo']
 757                  ],
 758                  'unusedtags' => ['unused1', 'unused2'],
 759                  'removeslottagids' => [3],
 760                  'expected' => [
 761                      1 => [],
 762                      2 => [],
 763                      3 => ['foo', 'bop'],
 764                      4 => ['bar', 'foo']
 765                  ]
 766              ]
 767          ];
 768      }
 769  
 770      /**
 771       * Test the quiz_retrieve_tags_for_slot_ids function with various parameter
 772       * combinations.
 773       *
 774       * @dataProvider get_quiz_retrieve_tags_for_slot_ids_test_cases()
 775       * @param int $questioncount The number of regular questions to create
 776       * @param int $randomquestioncount The number of random questions to create
 777       * @param array $randomquestiontags The tags for the random questions
 778       * @param string[] $unusedtags Additional tags to create to populate the DB with data
 779       * @param int[] $removeslottagids Slot numbers to remove tag ids for
 780       * @param array $expected The expected output of tag names indexed by slot number
 781       */
 782      public function test_quiz_retrieve_tags_for_slot_ids_combinations(
 783          $questioncount,
 784          $randomquestioncount,
 785          $randomquestiontags,
 786          $unusedtags,
 787          $removeslottagids,
 788          $expected
 789      ) {
 790          global $DB;
 791  
 792          $this->resetAfterTest();
 793          $this->setAdminUser();
 794  
 795          list($quiz, $tags) = $this->setup_quiz_and_tags(
 796              $questioncount,
 797              $randomquestioncount,
 798              $randomquestiontags,
 799              $unusedtags
 800          );
 801  
 802          $slots = $DB->get_records('quiz_slots', ['quizid' => $quiz->id]);
 803          $slotids = [];
 804          $slotsbynumber = [];
 805          foreach ($slots as $slot) {
 806              $slotids[] = $slot->id;
 807              $slotsbynumber[$slot->slot] = $slot;
 808          }
 809  
 810          if (!empty($removeslottagids)) {
 811              // The slots to remove are the slot numbers not the slot id so we need
 812              // to get the ids for the DB call.
 813              $idstonull = array_map(function($slot) use ($slotsbynumber) {
 814                  return $slotsbynumber[$slot]->id;
 815              }, $removeslottagids);
 816              list($sql, $params) = $DB->get_in_or_equal($idstonull);
 817              // Null out the tagid column to force the code to look up the tag by name.
 818              $DB->set_field_select('quiz_slot_tags', 'tagid', null, "slotid {$sql}", $params);
 819          }
 820  
 821          $slottagsbyslotids = quiz_retrieve_tags_for_slot_ids($slotids);
 822          // Convert the result into an associative array of slotid => [... tag names..]
 823          // to make it easier to compare.
 824          $actual = array_map(function($slottags) {
 825              $names = array_map(function($slottag) {
 826                  return $slottag->tagname;
 827              }, $slottags);
 828              // Make sure the names are sorted for comparison.
 829              sort($names);
 830              return $names;
 831          }, $slottagsbyslotids);
 832  
 833          $formattedexptected = [];
 834          // The expected values are indexed by slot number rather than id so let
 835          // convert it to use the id so that we can compare the results.
 836          foreach ($expected as $slot => $tagnames) {
 837              sort($tagnames);
 838              $slotid = $slotsbynumber[$slot]->id;
 839              $formattedexptected[$slotid] = $tagnames;
 840          }
 841  
 842          $this->assertEquals($formattedexptected, $actual);
 843      }
 844  }