Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

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

   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) /question/engine/statistics.php
  19   *
  20   * @package   quiz_statistics
  21   * @category  test
  22   * @copyright 2008 Jamie Pratt
  23   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace quiz_statistics;
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  global $CFG;
  31  require_once($CFG->libdir . '/questionlib.php');
  32  require_once($CFG->dirroot . '/mod/quiz/locallib.php');
  33  require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
  34  
  35  class testable_all_calculated_for_qubaid_condition extends \core_question\statistics\questions\all_calculated_for_qubaid_condition {
  36  
  37      /**
  38       * Disabling caching in tests so we are always sure to force the calculation of stats right then and there.
  39       *
  40       * @param qubaid_condition $qubaids
  41       */
  42      public function cache($qubaids) {
  43  
  44      }
  45  }
  46  
  47  /**
  48   * Test helper subclass of question_statistics
  49   *
  50   * @copyright 2010 The Open University
  51   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  52   */
  53  class testable_question_statistics extends \core_question\statistics\questions\calculator {
  54  
  55      /**
  56       * @var stdClass[]
  57       */
  58      protected $lateststeps;
  59  
  60      protected $statscollectionclassname = '\quiz_statistics\testable_all_calculated_for_qubaid_condition';
  61  
  62      public function set_step_data($states) {
  63          $this->lateststeps = $states;
  64      }
  65  
  66      protected function get_random_guess_score($questiondata) {
  67          return 0;
  68      }
  69  
  70      /**
  71       * @param $qubaids qubaid_condition is ignored in this test
  72       * @return array with two items
  73       *              - $lateststeps array of latest step data for the question usages
  74       *              - $summarks    array of total marks for each usage, indexed by usage id
  75       */
  76      protected function get_latest_steps($qubaids) {
  77          $summarks = [];
  78          $fakeusageid = 0;
  79          foreach ($this->lateststeps as $step) {
  80              // The same 'sumgrades' field is available in step data for every slot, we will ignore all slots but slot 1.
  81              // The step for slot 1 is always the first one in the csv file for each usage, we will use that to separate steps from
  82              // each usage.
  83              if ($step->slot == 1) {
  84                  $fakeusageid++;
  85                  $summarks[$fakeusageid] = $step->sumgrades;
  86              }
  87              unset($step->sumgrades);
  88              $step->questionusageid = $fakeusageid;
  89          }
  90  
  91          return [$this->lateststeps, $summarks];
  92      }
  93  
  94      protected function cache_stats($qubaids) {
  95          // No caching wanted for tests.
  96      }
  97  }
  98  /**
  99   * Unit tests for (some of) question_statistics.
 100   *
 101   * @copyright 2008 Jamie Pratt
 102   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 103   */
 104  class statistics_test extends \basic_testcase {
 105      /** @var testable_all_calculated_for_qubaid_condition object created to test class. */
 106      protected $qstats;
 107  
 108      public function test_qstats() {
 109          global $CFG;
 110          // Data is taken from randomly generated attempts data generated by
 111          // contrib/tools/generators/qagenerator/.
 112          $steps = $this->get_records_from_csv(__DIR__.'/fixtures/mdl_question_states.csv');
 113          // Data is taken from questions mostly generated by
 114          // contrib/tools/generators/generator.php.
 115          $questions = $this->get_records_from_csv(__DIR__.'/fixtures/mdl_question.csv');
 116          $calculator = new testable_question_statistics($questions);
 117          $calculator->set_step_data($steps);
 118          $this->qstats = $calculator->calculate(null);
 119  
 120          // Values expected are taken from contrib/tools/quiz_tools/stats.xls.
 121          $facility = [0, 0, 0, 0, null, null, null, 41.19318182, 81.36363636,
 122              71.36363636, 65.45454545, 65.90909091, 36.36363636, 59.09090909, 50,
 123              59.09090909, 63.63636364, 45.45454545, 27.27272727, 50];
 124          $this->qstats_q_fields('facility', $facility, 100);
 125          $sd = [0, 0, 0, 0, null, null, null, 1912.733589, 251.2738111,
 126              322.6312277, 333.4199022, 337.5811591, 492.3659639, 503.2362797,
 127              511.7663157, 503.2362797, 492.3659639, 509.6471914, 455.8423058, 511.7663157];
 128          $this->qstats_q_fields('sd', $sd, 1000);
 129          $effectiveweight = [0, 0, 0, 0, 0, 0, 0, 26.58464457, 3.368456046,
 130              3.253955259, 7.584083694, 3.79658376, 3.183278505, 4.532356904,
 131              7.78856243, 10.08351572, 8.381139345, 8.727645713, 7.946277111, 4.769500946];
 132          $this->qstats_q_fields('effectiveweight', $effectiveweight);
 133          $discriminationindex = [null, null, null, null, null, null, null,
 134              25.88327077, 1.170256965, -4.207816809, 28.16930644, -2.513606859,
 135              -12.99017581, -8.900638238, 8.670004606, 29.63337745, 15.18945843,
 136              16.21079629, 15.52451404, -8.396734802];
 137          $this->qstats_q_fields('discriminationindex', $discriminationindex);
 138          $discriminativeefficiency = [null, null, null, null, null, null, null,
 139              27.23492723, 1.382386552, -4.691171307, 31.12404354, -2.877487579,
 140              -17.5074184, -10.27568922, 10.86956522, 34.58997279, 17.4790556,
 141              20.14359793, 22.06477733, -10];
 142          $this->qstats_q_fields('discriminativeefficiency', $discriminativeefficiency);
 143      }
 144  
 145      public function qstats_q_fields($fieldname, $values, $multiplier=1) {
 146          foreach ($this->qstats->get_all_slots() as $slot) {
 147              $value = array_shift($values);
 148              if ($value !== null) {
 149                  $this->assertEqualsWithDelta($value, $this->qstats->for_slot($slot)->{$fieldname} * $multiplier, 1E-6);
 150              } else {
 151                  $this->assertEquals($value, $this->qstats->for_slot($slot)->{$fieldname} * $multiplier);
 152              }
 153          }
 154      }
 155  
 156      public function get_fields_from_csv($line) {
 157          $line = trim($line);
 158          $items = preg_split('!,!', $line);
 159          $cnt = count($items);
 160          for ($key = 0; $key < $cnt; $key++) {
 161              if ($items[$key]!='') {
 162                  if ($start = ($items[$key][0]=='"')) {
 163                      $items[$key] = substr($items[$key], 1);
 164                      while (!$end = ($items[$key][strlen($items[$key])-1]=='"')) {
 165                          $item = $items[$key];
 166                          unset($items[$key]);
 167                          $key++;
 168                          $items[$key] = $item . ',' . $items[$key];
 169                      }
 170                      $items[$key] = substr($items[$key], 0, strlen($items[$key])-1);
 171                  }
 172  
 173              }
 174          }
 175          return $items;
 176      }
 177  
 178      public function get_records_from_csv($filename) {
 179          $filecontents = file($filename, FILE_IGNORE_NEW_LINES);
 180          $records = [];
 181          // Skip the first line containing field names.
 182          $keys = $this->get_fields_from_csv(array_shift($filecontents));
 183          while (null !== ($line = array_shift($filecontents))) {
 184              $data = $this->get_fields_from_csv($line);
 185              $arraykey = reset($data);
 186              $object = new \stdClass();
 187              foreach ($keys as $key) {
 188                  $value = array_shift($data);
 189                  if ($value !== null) {
 190                      $object->{$key} = $value;
 191                  } else {
 192                      $object->{$key} = '';
 193                  }
 194              }
 195              $records[$arraykey] = $object;
 196          }
 197          return $records;
 198      }
 199  }