Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   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 quiz_statistics;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  
  21  global $CFG;
  22  require_once($CFG->dirroot . '/mod/quiz/report/statistics/report.php');
  23  require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
  24  require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
  25  require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
  26  
  27  /**
  28   * Tests for statistics report
  29   *
  30   * @package   quiz_statistics
  31   * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
  32   * @author    Mark Johnson <mark.johnson@catalyst-eu.net>
  33   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   * @covers    \quiz_statistics_report
  35   */
  36  class test_quiz_statistics_report extends \advanced_testcase {
  37  
  38      use \quiz_question_helper_test_trait;
  39  
  40      /**
  41       * Secondary database connection for creating locks.
  42       *
  43       * @var \moodle_database|null
  44       */
  45      protected static ?\moodle_database $lockdb;
  46  
  47      /**
  48       * Lock factory using the secondary database connection.
  49       *
  50       * @var \moodle_database|null
  51       */
  52      protected static ?\core\lock\lock_factory $lockfactory;
  53  
  54      /**
  55       * Create a lock factory with a second database session.
  56       *
  57       * This allows us to create a lock in our test code that will block a lock request
  58       * on the same key in code under test.
  59       *
  60       * @return void
  61       */
  62      public static function setUpBeforeClass(): void {
  63          global $CFG;
  64          self::$lockdb = \moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
  65          self::$lockdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->prefix, $CFG->dboptions);
  66          $lockfactory = \core\lock\lock_config::get_lock_factory('quiz_statistics_get_stats');
  67          $reflectiondb = new \ReflectionProperty($lockfactory, 'db');
  68          $reflectiondb->setAccessible(true);
  69          $reflectiondb->setValue($lockfactory, self::$lockdb);
  70          self::$lockfactory = $lockfactory;
  71      }
  72  
  73      /**
  74       * Dispose of the extra DB connection and lock factory.
  75       *
  76       * @return void
  77       */
  78      public static function tearDownAfterClass(): void {
  79          self::$lockdb->dispose();
  80          self::$lockdb = null;
  81          self::$lockfactory = null;
  82      }
  83  
  84      /**
  85       * Return a generated quiz
  86       *
  87       * @return \stdClass
  88       */
  89      protected function create_and_attempt_quiz(): \stdClass {
  90          $course = $this->getDataGenerator()->create_course();
  91          $user = $this->getDataGenerator()->create_user();
  92          $quiz = $this->create_test_quiz($course);
  93          $quizcontext = \context_module::instance($quiz->cmid);
  94          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
  95          $this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $quizcontext->id]);
  96          $this->attempt_quiz($quiz, $user);
  97  
  98          return $quiz;
  99      }
 100  
 101      /**
 102       * Test locking the calculation process.
 103       *
 104       * When there is a lock on the hash code, test_get_all_stats_and_analysis() should wait until the lock timeout, then throw an
 105       * exception.
 106       *
 107       * When there is no lock (or the lock has been released), it should return a result.
 108       *
 109       * @return void
 110       */
 111      public function test_get_all_stats_and_analysis_locking(): void {
 112          $this->resetAfterTest(true);
 113          $quiz = $this->create_and_attempt_quiz();
 114          $whichattempts = QUIZ_GRADEAVERAGE; // All attempts.
 115          $whichtries = \question_attempt::ALL_TRIES;
 116          $groupstudentsjoins = new \core\dml\sql_join();
 117          $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudentsjoins, $whichattempts);
 118  
 119          $report = new \quiz_statistics_report();
 120          $questions = $report->load_and_initialise_questions_for_calculations($quiz);
 121  
 122          $timeoutseconds = 20;
 123          set_config('getstatslocktimeout', $timeoutseconds, 'quiz_statistics');
 124          $lock = self::$lockfactory->get_lock($qubaids->get_hash_code(), 0);
 125  
 126          $progress = new \core\progress\none();
 127  
 128          $this->resetDebugging();
 129          $timebefore = microtime(true);
 130          try {
 131              $result = $report->get_all_stats_and_analysis(
 132                  $quiz,
 133                  $whichattempts,
 134                  $whichtries,
 135                  $groupstudentsjoins,
 136                  $questions,
 137                  $progress
 138              );
 139              $timeafter = microtime(true);
 140  
 141              // Verify that we waited as long as the timeout.
 142              $this->assertEqualsWithDelta($timeoutseconds, $timeafter - $timebefore, 1);
 143              $this->assertDebuggingCalled('Could not get lock on ' .
 144                      $qubaids->get_hash_code() . ' (Quiz ID ' . $quiz->id . ') after ' .
 145                      $timeoutseconds . ' seconds');
 146              $this->assertEquals([null, null], $result);
 147          } finally {
 148              $lock->release();
 149          }
 150  
 151          $this->resetDebugging();
 152          $result = $report->get_all_stats_and_analysis(
 153              $quiz,
 154              $whichattempts,
 155              $whichtries,
 156              $groupstudentsjoins,
 157              $questions
 158          );
 159          $this->assertDebuggingNotCalled();
 160          $this->assertNotEquals([null, null], $result);
 161      }
 162  
 163      /**
 164       * Test locking when the current page does not require calculations.
 165       *
 166       * When there is a lock on the hash code, test_get_all_stats_and_analysis() should return a null result immediately,
 167       * with no exception thrown.
 168       *
 169       * @return void
 170       */
 171      public function test_get_all_stats_and_analysis_locking_no_calculation(): void {
 172          $this->resetAfterTest(true);
 173          $quiz = $this->create_and_attempt_quiz();
 174  
 175          $whichattempts = QUIZ_GRADEAVERAGE; // All attempts.
 176          $whichtries = \question_attempt::ALL_TRIES;
 177          $groupstudentsjoins = new \core\dml\sql_join();
 178          $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudentsjoins, $whichattempts);
 179  
 180          $report = new \quiz_statistics_report();
 181          $questions = $report->load_and_initialise_questions_for_calculations($quiz);
 182  
 183          $timeoutseconds = 20;
 184          set_config('getstatslocktimeout', $timeoutseconds, 'quiz_statistics');
 185  
 186          $lock = self::$lockfactory->get_lock($qubaids->get_hash_code(), 0);
 187  
 188          $this->resetDebugging();
 189          try {
 190              $progress = new \core\progress\none();
 191  
 192              $timebefore = microtime(true);
 193              $result = $report->get_all_stats_and_analysis(
 194                  $quiz,
 195                  $whichattempts,
 196                  $whichtries,
 197                  $groupstudentsjoins,
 198                  $questions,
 199                  $progress,
 200                  false
 201              );
 202              $timeafter = microtime(true);
 203  
 204              // Verify that we did not wait for the timeout before returning.
 205              $this->assertLessThan($timeoutseconds, $timeafter - $timebefore);
 206              $this->assertEquals([null, null], $result);
 207              $this->assertDebuggingNotCalled();
 208          } finally {
 209              $lock->release();
 210          }
 211      }
 212  }