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 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_question\local\statistics;
  18  
  19  use advanced_testcase;
  20  use context;
  21  use context_module;
  22  use core_question\statistics\questions\all_calculated_for_qubaid_condition;
  23  use core_question_generator;
  24  use Generator;
  25  use quiz;
  26  use quiz_attempt;
  27  use question_engine;
  28  use ReflectionMethod;
  29  
  30  /**
  31   * Tests for question statistics.
  32   *
  33   * @package   core_question
  34   * @copyright 2021 Catalyst IT Australia Pty Ltd
  35   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   * @covers \core_question\local\statistics\statistics_bulk_loader
  37   */
  38  class statistics_bulk_loader_test extends advanced_testcase {
  39  
  40      /** @var float Delta used when comparing statistics values out-of 1. */
  41      protected const DELTA = 0.00005;
  42  
  43      /** @var float Delta used when comparing statistics values out-of 100. */
  44      protected const PERCENT_DELTA = 0.005;
  45  
  46      /**
  47       * Test quizzes that contain a specified question.
  48       *
  49       * @covers ::get_all_places_where_questions_were_attempted
  50       */
  51      public function test_get_all_places_where_questions_were_attempted(): void {
  52          $this->resetAfterTest();
  53          $this->setAdminUser();
  54  
  55          $rcm = new ReflectionMethod(statistics_bulk_loader::class, 'get_all_places_where_questions_were_attempted');
  56          $rcm->setAccessible(true);
  57  
  58          // Create a course.
  59          $course = $this->getDataGenerator()->create_course();
  60  
  61          // Create three quizzes.
  62          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
  63          $quiz1 = $quizgenerator->create_instance([
  64              'course' => $course->id,
  65              'grade' => 100.0, 'sumgrades' => 2,
  66              'layout' => '1,2,0'
  67          ]);
  68          $quiz1context = context_module::instance($quiz1->cmid);
  69  
  70          $quiz2 = $quizgenerator->create_instance([
  71              'course' => $course->id,
  72              'grade' => 100.0, 'sumgrades' => 2,
  73              'layout' => '1,2,0'
  74          ]);
  75          $quiz2context = context_module::instance($quiz2->cmid);
  76  
  77          $quiz3 = $quizgenerator->create_instance([
  78              'course' => $course->id,
  79              'grade' => 100.0, 'sumgrades' => 2,
  80              'layout' => '1,2,0'
  81          ]);
  82          $quiz3context = context_module::instance($quiz3->cmid);
  83  
  84          // Create questions.
  85          /** @var core_question_generator $questiongenerator */
  86          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
  87          $cat = $questiongenerator->create_question_category();
  88          $question1 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
  89          $question2 = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
  90  
  91          // Add question 1 to quiz 1 and make an attempt.
  92          quiz_add_quiz_question($question1->id, $quiz1);
  93          // Quiz 1 attempt.
  94          $this->submit_quiz($quiz1, [1 => ['answer' => 'frog']]);
  95  
  96          // Add questions 1 and 2 to quiz 2.
  97          quiz_add_quiz_question($question1->id, $quiz2);
  98          quiz_add_quiz_question($question2->id, $quiz2);
  99          $this->submit_quiz($quiz2, [1 => ['answer' => 'frog'], 2 => ['answer' => 10]]);
 100  
 101          // Checking quizzes that use question 1.
 102          $q1places = $rcm->invoke(null, [$question1->id]);
 103          $this->assertCount(2, $q1places);
 104          $this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz1context->id], $q1places[0]);
 105          $this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz2context->id], $q1places[1]);
 106  
 107          // Checking quizzes that contain question 2.
 108          $q2places = $rcm->invoke(null, [$question2->id]);
 109          $this->assertCount(1, $q2places);
 110          $this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz2context->id], $q2places[0]);
 111  
 112          // Add a random question to quiz3.
 113          quiz_add_random_questions($quiz3, 0, $cat->id, 1, false);
 114          $this->submit_quiz($quiz3, [1 => ['answer' => 'willbewrong']]);
 115  
 116          // Quiz 3 will now be in one of these arrays.
 117          $q1places = $rcm->invoke(null, [$question1->id]);
 118          $q2places = $rcm->invoke(null, [$question2->id]);
 119          if (count($q1places) == 3) {
 120              $newplace = end($q1places);
 121          } else {
 122              $newplace = end($q2places);
 123          }
 124          $this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz3context->id], $newplace);
 125      }
 126  
 127      /**
 128       * Create 2 quizzes.
 129       *
 130       * @return array return 2 quizzes
 131       */
 132      private function prepare_quizzes(): array {
 133          // Create a course.
 134          $course = $this->getDataGenerator()->create_course();
 135  
 136          // Make 2 quizzes.
 137          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 138          $layout = '1,2,0,3,4,0';
 139          $quiz1 = $quizgenerator->create_instance([
 140              'course' => $course->id,
 141              'grade' => 100.0, 'sumgrades' => 2,
 142              'layout' => $layout
 143          ]);
 144  
 145          $quiz2 = $quizgenerator->create_instance([
 146              'course' => $course->id,
 147              'grade' => 100.0, 'sumgrades' => 2,
 148              'layout' => $layout
 149          ]);
 150  
 151          /** @var core_question_generator $questiongenerator */
 152          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 153          $cat = $questiongenerator->create_question_category();
 154  
 155          $page = 1;
 156          $questions = [];
 157          foreach (explode(',', $layout) as $slot) {
 158              if ($slot == 0) {
 159                  $page += 1;
 160                  continue;
 161              }
 162  
 163              $question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 164              $questions[$slot] = $question;
 165              quiz_add_quiz_question($question->id, $quiz1, $page);
 166              quiz_add_quiz_question($question->id, $quiz2, $page);
 167          }
 168  
 169          return [$quiz1, $quiz2, $questions];
 170      }
 171  
 172      /**
 173       * Submit quiz answers
 174       *
 175       * @param object $quiz
 176       * @param array $answers
 177       */
 178      private function submit_quiz(object $quiz, array $answers): void {
 179          // Create user.
 180          $user = $this->getDataGenerator()->create_user();
 181          // Create attempt.
 182          $quizobj = quiz::create($quiz->id, $user->id);
 183          $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 184          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 185          $timenow = time();
 186          $attempt = quiz_create_attempt($quizobj, 1, null, $timenow, false, $user->id);
 187          quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
 188          quiz_attempt_save_started($quizobj, $quba, $attempt);
 189          // Submit attempt.
 190          $attemptobj = quiz_attempt::create($attempt->id);
 191          $attemptobj->process_submitted_actions($timenow, false, $answers);
 192          $attemptobj->process_finish($timenow, false);
 193      }
 194  
 195      /**
 196       * Generate attempt answers.
 197       *
 198       * @param array $correctanswerflags array of 1 or 0
 199       * 1 : generate correct answer
 200       * 0 : generate wrong answer
 201       *
 202       * @return array
 203       */
 204      private function generate_attempt_answers(array $correctanswerflags): array {
 205          $attempt = [];
 206          for ($i = 1; $i <= 4; $i++) {
 207              if (isset($correctanswerflags) && $correctanswerflags[$i - 1] == 1) {
 208                  // Correct answer.
 209                  $attempt[$i] = ['answer' => 'frog'];
 210              } else {
 211                  $attempt[$i] = ['answer' => 'false'];
 212              }
 213          }
 214          return $attempt;
 215      }
 216  
 217      /**
 218       * Generate quizzes and submit answers.
 219       *
 220       * @param array $quiz1attempts quiz 1 attempts
 221       * @param array $quiz2attempts quiz 2 attempts
 222       *
 223       * @return array
 224       */
 225      private function prepare_and_submit_quizzes(array $quiz1attempts, array $quiz2attempts): array {
 226          list($quiz1, $quiz2, $questions) = $this->prepare_quizzes();
 227          // Submit attempts of quiz1.
 228          foreach ($quiz1attempts as $attempt) {
 229              $this->submit_quiz($quiz1, $attempt);
 230          }
 231          // Submit attempts of quiz2.
 232          foreach ($quiz2attempts as $attempt) {
 233              $this->submit_quiz($quiz2, $attempt);
 234          }
 235  
 236          // Calculate the statistics.
 237          $this->expectOutputRegex('~.*Calculations completed.*~');
 238          $statisticstask = new \quiz_statistics\task\recalculate();
 239          $statisticstask->execute();
 240  
 241          return [$quiz1, $quiz2, $questions];
 242      }
 243  
 244      /**
 245       * To use private helper::extract_item_value function.
 246       *
 247       * @param all_calculated_for_qubaid_condition $statistics the batch of statistics.
 248       * @param int $questionid a question id.
 249       * @param string $item one of the field names in all_calculated_for_qubaid_condition, e.g. 'facility'.
 250       * @return float|null the required value.
 251       */
 252      private function extract_item_value(all_calculated_for_qubaid_condition $statistics,
 253                                          int $questionid, string $item): ?float {
 254          $rcm = new ReflectionMethod(statistics_bulk_loader::class, 'extract_item_value');
 255          $rcm->setAccessible(true);
 256          return $rcm->invoke(null, $statistics, $questionid, $item);
 257      }
 258  
 259      /**
 260       * To use private helper::load_statistics_for_place function (with mod_quiz component).
 261       *
 262       * @param context $context the context to load the statistics for.
 263       * @return all_calculated_for_qubaid_condition|null question statistics.
 264       */
 265      private function load_quiz_statistics_for_place(context $context): ?all_calculated_for_qubaid_condition {
 266          $rcm = new ReflectionMethod(statistics_bulk_loader::class, 'load_statistics_for_place');
 267          $rcm->setAccessible(true);
 268          return $rcm->invoke(null, 'mod_quiz', $context);
 269      }
 270  
 271      /**
 272       * Data provider for {@see test_load_question_facility()}.
 273       *
 274       * @return Generator
 275       */
 276      public function load_question_facility_provider(): Generator {
 277          yield 'Facility case 1' => [
 278              'Quiz 1 attempts' => [
 279                  $this->generate_attempt_answers([1, 0, 0, 0]),
 280              ],
 281              'Expected quiz 1 facilities' => [1.0, 0.0, 0.0, 0.0],
 282              'Quiz 2 attempts' => [
 283                  $this->generate_attempt_answers([1, 0, 0, 0]),
 284                  $this->generate_attempt_answers([1, 1, 0, 0]),
 285              ],
 286              'Expected quiz 2 facilities' => [1.0, 0.5, 0.0, 0.0],
 287              'Expected average facilities' => [1.0, 0.25, 0.0, 0.0],
 288          ];
 289          yield 'Facility case 2' => [
 290              'Quiz 1 attempts' => [
 291                  $this->generate_attempt_answers([1, 0, 0, 0]),
 292                  $this->generate_attempt_answers([1, 1, 0, 0]),
 293                  $this->generate_attempt_answers([1, 1, 1, 0]),
 294              ],
 295              'Expected quiz 1 facilities' => [1.0, 0.6667, 0.3333, 0.0],
 296              'Quiz 2 attempts' => [
 297                  $this->generate_attempt_answers([1, 0, 0, 0]),
 298                  $this->generate_attempt_answers([1, 1, 0, 0]),
 299                  $this->generate_attempt_answers([1, 1, 1, 0]),
 300                  $this->generate_attempt_answers([1, 1, 1, 1]),
 301              ],
 302              'Expected quiz 2 facilities' => [1.0, 0.75, 0.5, 0.25],
 303              'Expected average facilities' => [1.0, 0.7083, 0.4167, 0.1250],
 304          ];
 305      }
 306  
 307      /**
 308       * Test question facility
 309       *
 310       * @dataProvider load_question_facility_provider
 311       *
 312       * @param array $quiz1attempts quiz 1 attempts
 313       * @param array $expectedquiz1facilities expected quiz 1 facilities
 314       * @param array $quiz2attempts quiz 2 attempts
 315       * @param array $expectedquiz2facilities  expected quiz 2 facilities
 316       * @param array $expectedaveragefacilities expected average facilities
 317       */
 318      public function test_load_question_facility(
 319          array $quiz1attempts,
 320          array $expectedquiz1facilities,
 321          array $quiz2attempts,
 322          array $expectedquiz2facilities,
 323          array $expectedaveragefacilities)
 324      : void {
 325          $this->resetAfterTest();
 326  
 327          list($quiz1, $quiz2, $questions) = $this->prepare_and_submit_quizzes($quiz1attempts, $quiz2attempts);
 328  
 329          // Quiz 1 facilities.
 330          $stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz1->cmid));
 331          $quiz1facility1 = $this->extract_item_value($stats, $questions[1]->id, 'facility');
 332          $quiz1facility2 = $this->extract_item_value($stats, $questions[2]->id, 'facility');
 333          $quiz1facility3 = $this->extract_item_value($stats, $questions[3]->id, 'facility');
 334          $quiz1facility4 = $this->extract_item_value($stats, $questions[4]->id, 'facility');
 335  
 336          $this->assertEqualsWithDelta($expectedquiz1facilities[0], $quiz1facility1, self::DELTA);
 337          $this->assertEqualsWithDelta($expectedquiz1facilities[1], $quiz1facility2, self::DELTA);
 338          $this->assertEqualsWithDelta($expectedquiz1facilities[2], $quiz1facility3, self::DELTA);
 339          $this->assertEqualsWithDelta($expectedquiz1facilities[3], $quiz1facility4, self::DELTA);
 340  
 341          // Quiz 2 facilities.
 342          $stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz2->cmid));
 343          $quiz2facility1 = $this->extract_item_value($stats, $questions[1]->id, 'facility');
 344          $quiz2facility2 = $this->extract_item_value($stats, $questions[2]->id, 'facility');
 345          $quiz2facility3 = $this->extract_item_value($stats, $questions[3]->id, 'facility');
 346          $quiz2facility4 = $this->extract_item_value($stats, $questions[4]->id, 'facility');
 347  
 348          $this->assertEqualsWithDelta($expectedquiz2facilities[0], $quiz2facility1, self::DELTA);
 349          $this->assertEqualsWithDelta($expectedquiz2facilities[1], $quiz2facility2, self::DELTA);
 350          $this->assertEqualsWithDelta($expectedquiz2facilities[2], $quiz2facility3, self::DELTA);
 351          $this->assertEqualsWithDelta($expectedquiz2facilities[3], $quiz2facility4, self::DELTA);
 352  
 353          // Average question facilities.
 354          $stats = statistics_bulk_loader::load_aggregate_statistics(
 355              [$questions[1]->id, $questions[2]->id, $questions[3]->id, $questions[4]->id],
 356              ['facility']
 357          );
 358  
 359          $this->assertEqualsWithDelta($expectedaveragefacilities[0],
 360              $stats[$questions[1]->id]['facility'], self::DELTA);
 361          $this->assertEqualsWithDelta($expectedaveragefacilities[1],
 362              $stats[$questions[2]->id]['facility'], self::DELTA);
 363          $this->assertEqualsWithDelta($expectedaveragefacilities[2],
 364              $stats[$questions[3]->id]['facility'], self::DELTA);
 365          $this->assertEqualsWithDelta($expectedaveragefacilities[3],
 366              $stats[$questions[4]->id]['facility'], self::DELTA);
 367      }
 368  
 369      /**
 370       * Data provider for {@see test_load_question_discriminative_efficiency()}.
 371       * @return Generator
 372       */
 373      public function load_question_discriminative_efficiency_provider(): Generator {
 374          yield 'Discriminative efficiency' => [
 375              'Quiz 1 attempts' => [
 376                  $this->generate_attempt_answers([1, 0, 0, 0]),
 377                  $this->generate_attempt_answers([1, 1, 0, 0]),
 378                  $this->generate_attempt_answers([1, 0, 1, 0]),
 379                  $this->generate_attempt_answers([1, 1, 1, 1]),
 380              ],
 381              'Expected quiz 1 discriminative efficiency' => [null, 33.33, 33.33, 100.00],
 382              'Quiz 2 attempts' => [
 383                  $this->generate_attempt_answers([1, 1, 1, 1]),
 384                  $this->generate_attempt_answers([0, 0, 0, 0]),
 385                  $this->generate_attempt_answers([1, 0, 0, 1]),
 386                  $this->generate_attempt_answers([0, 1, 1, 0]),
 387              ],
 388              'Expected quiz 2 discriminative efficiency' => [50.00, 50.00, 50.00, 50.00],
 389              'Expected average discriminative efficiency' => [50.00, 41.67, 41.67, 75.00],
 390          ];
 391      }
 392  
 393      /**
 394       * Test discriminative efficiency
 395       *
 396       * @dataProvider load_question_discriminative_efficiency_provider
 397       *
 398       * @param array $quiz1attempts quiz 1 attempts
 399       * @param array $expectedquiz1discriminativeefficiency expected quiz 1 discriminative efficiency
 400       * @param array $quiz2attempts quiz 2 attempts
 401       * @param array $expectedquiz2discriminativeefficiency expected quiz 2 discriminative efficiency
 402       * @param array $expectedaveragediscriminativeefficiency expected average discriminative efficiency
 403       */
 404      public function test_load_question_discriminative_efficiency(
 405          array $quiz1attempts,
 406          array $expectedquiz1discriminativeefficiency,
 407          array $quiz2attempts,
 408          array $expectedquiz2discriminativeefficiency,
 409          array $expectedaveragediscriminativeefficiency
 410      ): void {
 411          $this->resetAfterTest();
 412  
 413          list($quiz1, $quiz2, $questions) = $this->prepare_and_submit_quizzes($quiz1attempts, $quiz2attempts);
 414  
 415          // Quiz 1 discriminative efficiency.
 416          $stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz1->cmid));
 417          $discriminativeefficiency1 = $this->extract_item_value($stats, $questions[1]->id, 'discriminativeefficiency');
 418          $discriminativeefficiency2 = $this->extract_item_value($stats, $questions[2]->id, 'discriminativeefficiency');
 419          $discriminativeefficiency3 = $this->extract_item_value($stats, $questions[3]->id, 'discriminativeefficiency');
 420          $discriminativeefficiency4 = $this->extract_item_value($stats, $questions[4]->id, 'discriminativeefficiency');
 421  
 422          $this->assertEqualsWithDelta($expectedquiz1discriminativeefficiency[0],
 423                  $discriminativeefficiency1, self::PERCENT_DELTA);
 424          $this->assertEqualsWithDelta($expectedquiz1discriminativeefficiency[1],
 425                  $discriminativeefficiency2, self::PERCENT_DELTA);
 426          $this->assertEqualsWithDelta($expectedquiz1discriminativeefficiency[2],
 427                  $discriminativeefficiency3, self::PERCENT_DELTA);
 428          $this->assertEqualsWithDelta($expectedquiz1discriminativeefficiency[3],
 429                  $discriminativeefficiency4, self::PERCENT_DELTA);
 430  
 431          // Quiz 2 discriminative efficiency.
 432          $stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz2->cmid));
 433          $discriminativeefficiency1 = $this->extract_item_value($stats, $questions[1]->id, 'discriminativeefficiency');
 434          $discriminativeefficiency2 = $this->extract_item_value($stats, $questions[2]->id, 'discriminativeefficiency');
 435          $discriminativeefficiency3 = $this->extract_item_value($stats, $questions[3]->id, 'discriminativeefficiency');
 436          $discriminativeefficiency4 = $this->extract_item_value($stats, $questions[4]->id, 'discriminativeefficiency');
 437  
 438          $this->assertEqualsWithDelta($expectedquiz2discriminativeefficiency[0],
 439                  $discriminativeefficiency1, self::PERCENT_DELTA);
 440          $this->assertEqualsWithDelta($expectedquiz2discriminativeefficiency[1],
 441                  $discriminativeefficiency2, self::PERCENT_DELTA);
 442          $this->assertEqualsWithDelta($expectedquiz2discriminativeefficiency[2],
 443                  $discriminativeefficiency3, self::PERCENT_DELTA);
 444          $this->assertEqualsWithDelta($expectedquiz2discriminativeefficiency[3],
 445                  $discriminativeefficiency4, self::PERCENT_DELTA);
 446  
 447          // Average question discriminative efficiency.
 448          $stats = statistics_bulk_loader::load_aggregate_statistics(
 449              [$questions[1]->id, $questions[2]->id, $questions[3]->id, $questions[4]->id],
 450              ['discriminativeefficiency']
 451          );
 452  
 453          $this->assertEqualsWithDelta($expectedaveragediscriminativeefficiency[0],
 454              $stats[$questions[1]->id]['discriminativeefficiency'], self::PERCENT_DELTA);
 455          $this->assertEqualsWithDelta($expectedaveragediscriminativeefficiency[1],
 456              $stats[$questions[2]->id]['discriminativeefficiency'], self::PERCENT_DELTA);
 457          $this->assertEqualsWithDelta($expectedaveragediscriminativeefficiency[2],
 458              $stats[$questions[3]->id]['discriminativeefficiency'], self::PERCENT_DELTA);
 459          $this->assertEqualsWithDelta($expectedaveragediscriminativeefficiency[3],
 460              $stats[$questions[4]->id]['discriminativeefficiency'], self::PERCENT_DELTA);
 461      }
 462  
 463      /**
 464       * Data provider for {@see test_load_question_discrimination_index()}.
 465       * @return Generator
 466       */
 467      public function load_question_discrimination_index_provider(): Generator {
 468          yield 'Discrimination Index' => [
 469              'Quiz 1 attempts' => [
 470                  $this->generate_attempt_answers([1, 0, 0, 0]),
 471                  $this->generate_attempt_answers([1, 1, 0, 0]),
 472                  $this->generate_attempt_answers([1, 0, 1, 0]),
 473                  $this->generate_attempt_answers([1, 1, 1, 1]),
 474              ],
 475              'Expected quiz 1 Discrimination Index' => [null, 30.15, 30.15, 81.65],
 476              'Quiz 2 attempts' => [
 477                  $this->generate_attempt_answers([1, 1, 1, 1]),
 478                  $this->generate_attempt_answers([0, 0, 0, 0]),
 479                  $this->generate_attempt_answers([1, 0, 0, 1]),
 480                  $this->generate_attempt_answers([0, 1, 1, 0]),
 481              ],
 482              'Expected quiz 2 discrimination Index' => [44.72, 44.72, 44.72, 44.72],
 483              'Expected average discrimination Index' => [44.72, 37.44, 37.44, 63.19],
 484          ];
 485      }
 486  
 487      /**
 488       * Test discrimination index
 489       *
 490       * @dataProvider load_question_discrimination_index_provider
 491       *
 492       * @param array $quiz1attempts quiz 1 attempts
 493       * @param array $expectedquiz1discriminationindex expected quiz 1 discrimination index
 494       * @param array $quiz2attempts quiz 2 attempts
 495       * @param array $expectedquiz2discriminationindex expected quiz 2 discrimination index
 496       * @param array $expectedaveragediscriminationindex expected average discrimination index
 497       */
 498      public function test_load_question_discrimination_index(
 499          array $quiz1attempts,
 500          array $expectedquiz1discriminationindex,
 501          array $quiz2attempts,
 502          array $expectedquiz2discriminationindex,
 503          array $expectedaveragediscriminationindex
 504      ): void {
 505          $this->resetAfterTest();
 506  
 507          list($quiz1, $quiz2, $questions) = $this->prepare_and_submit_quizzes($quiz1attempts, $quiz2attempts);
 508  
 509          // Quiz 1 discrimination index.
 510          $stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz1->cmid));
 511          $discriminationindex1 = $this->extract_item_value($stats, $questions[1]->id, 'discriminationindex');
 512          $discriminationindex2 = $this->extract_item_value($stats, $questions[2]->id, 'discriminationindex');
 513          $discriminationindex3 = $this->extract_item_value($stats, $questions[3]->id, 'discriminationindex');
 514          $discriminationindex4 = $this->extract_item_value($stats, $questions[4]->id, 'discriminationindex');
 515  
 516          $this->assertEqualsWithDelta($expectedquiz1discriminationindex[0],
 517              $discriminationindex1, self::PERCENT_DELTA);
 518          $this->assertEqualsWithDelta($expectedquiz1discriminationindex[1],
 519              $discriminationindex2, self::PERCENT_DELTA);
 520          $this->assertEqualsWithDelta($expectedquiz1discriminationindex[2],
 521              $discriminationindex3, self::PERCENT_DELTA);
 522          $this->assertEqualsWithDelta($expectedquiz1discriminationindex[3],
 523              $discriminationindex4, self::PERCENT_DELTA);
 524  
 525          // Quiz 2 discrimination index.
 526          $stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz2->cmid));
 527          $discriminationindex1 = $this->extract_item_value($stats, $questions[1]->id, 'discriminationindex');
 528          $discriminationindex2 = $this->extract_item_value($stats, $questions[2]->id, 'discriminationindex');
 529          $discriminationindex3 = $this->extract_item_value($stats, $questions[3]->id, 'discriminationindex');
 530          $discriminationindex4 = $this->extract_item_value($stats, $questions[4]->id, 'discriminationindex');
 531  
 532          $this->assertEqualsWithDelta($expectedquiz2discriminationindex[0],
 533              $discriminationindex1, self::PERCENT_DELTA);
 534          $this->assertEqualsWithDelta($expectedquiz2discriminationindex[1],
 535              $discriminationindex2, self::PERCENT_DELTA);
 536          $this->assertEqualsWithDelta($expectedquiz2discriminationindex[2],
 537              $discriminationindex3, self::PERCENT_DELTA);
 538          $this->assertEqualsWithDelta($expectedquiz2discriminationindex[3],
 539              $discriminationindex4, self::PERCENT_DELTA);
 540  
 541          // Average question discrimination index.
 542          $stats = statistics_bulk_loader::load_aggregate_statistics(
 543              [$questions[1]->id, $questions[2]->id, $questions[3]->id, $questions[4]->id],
 544              ['discriminationindex']
 545          );
 546  
 547          $this->assertEqualsWithDelta($expectedaveragediscriminationindex[0],
 548              $stats[$questions[1]->id]['discriminationindex'], self::PERCENT_DELTA);
 549          $this->assertEqualsWithDelta($expectedaveragediscriminationindex[1],
 550              $stats[$questions[2]->id]['discriminationindex'], self::PERCENT_DELTA);
 551          $this->assertEqualsWithDelta($expectedaveragediscriminationindex[2],
 552              $stats[$questions[3]->id]['discriminationindex'], self::PERCENT_DELTA);
 553          $this->assertEqualsWithDelta($expectedaveragediscriminationindex[3],
 554              $stats[$questions[4]->id]['discriminationindex'], self::PERCENT_DELTA);
 555      }
 556  
 557      /**
 558       * Test with question statistics disabled
 559       */
 560      public function test_statistics_disabled(): void {
 561          $this->resetAfterTest();
 562  
 563          // Prepare some quizzes and attempts. Exactly what is not important to this test.
 564          $quiz1attempts = [$this->generate_attempt_answers([1, 0, 0, 0])];
 565          $quiz2attempts = [$this->generate_attempt_answers([1, 1, 1, 1])];
 566          [, , $questions] = $this->prepare_and_submit_quizzes($quiz1attempts, $quiz2attempts);
 567  
 568          // Prepare some useful arrays.
 569          $expectedstats = [
 570              $questions[1]->id => [],
 571              $questions[2]->id => [],
 572              $questions[3]->id => [],
 573              $questions[4]->id => [],
 574          ];
 575          $questionids = array_keys($expectedstats);
 576  
 577          // Ask to load no statistics at all.
 578          $stats = statistics_bulk_loader::load_aggregate_statistics($questionids, []);
 579  
 580          // Verify we got the right thing.
 581          $this->assertEquals($expectedstats, $stats);
 582      }
 583  }