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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body