Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 311 and 403] [Versions 400 and 403] [Versions 401 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_analytics;
  18  
  19  use test_timesplitting_seconds;
  20  use test_timesplitting_upcoming_seconds;
  21  
  22  defined('MOODLE_INTERNAL') || die();
  23  
  24  require_once (__DIR__ . '/fixtures/test_timesplitting_seconds.php');
  25  require_once (__DIR__ . '/fixtures/test_timesplitting_upcoming_seconds.php');
  26  require_once (__DIR__ . '/../../lib/enrollib.php');
  27  
  28  /**
  29   * Unit tests for core time splitting methods.
  30   *
  31   * @package   core
  32   * @category  test
  33   * @copyright 2017 David MonllaĆ³ {@link http://www.davidmonllao.com}
  34   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class time_splittings_test extends \advanced_testcase {
  37  
  38      /** @var \stdClass course record. */
  39      protected $course;
  40  
  41      /** @var course Moodle course analysable. */
  42      protected $analysable;
  43  
  44      /**
  45       * setUp
  46       *
  47       * @return void
  48       */
  49      public function setUp(): void {
  50  
  51          $this->resetAfterTest(true);
  52  
  53          // Generate training data.
  54          $params = array(
  55              'startdate' => mktime(8, 15, 32, 10, 24, 2015),
  56              'enddate' => mktime(12, 12, 31, 10, 24, 2016),
  57          );
  58          $this->course = $this->getDataGenerator()->create_course($params);
  59          $this->analysable = new \core_analytics\course($this->course);
  60      }
  61  
  62      /**
  63       * test_ranges
  64       *
  65       * @return void
  66       */
  67      public function test_valid_ranges() {
  68  
  69          // All core time splitting methods.
  70          $timesplittings = array(
  71              '\core\analytics\time_splitting\deciles',
  72              '\core\analytics\time_splitting\deciles_accum',
  73              '\core\analytics\time_splitting\no_splitting',
  74              '\core\analytics\time_splitting\quarters',
  75              '\core\analytics\time_splitting\quarters_accum',
  76              '\core\analytics\time_splitting\single_range',
  77              '\core\analytics\time_splitting\upcoming_week',
  78          );
  79  
  80          // Check that defined ranges are valid (tested through validate_ranges).
  81          foreach ($timesplittings as $timesplitting) {
  82              $instance = new $timesplitting();
  83              $instance->set_analysable($this->analysable);
  84          }
  85      }
  86  
  87      /**
  88       * test_range_dates
  89       *
  90       * @return void
  91       */
  92      public function test_range_dates() {
  93  
  94          $nov2015 = mktime(0, 0, 0, 11, 24, 2015);
  95          $aug2016 = mktime(0, 0, 0, 8, 29, 2016);
  96  
  97          // Equal parts.
  98          $quarters = new \core\analytics\time_splitting\quarters();
  99          $quarters->set_analysable($this->analysable);
 100          $ranges = $quarters->get_all_ranges();
 101          $this->assertCount(4, $ranges);
 102          $this->assertCount(4, $quarters->get_training_ranges());
 103          $this->assertCount(4, $quarters->get_distinct_ranges());
 104  
 105          $this->assertGreaterThan($ranges[0]['start'], $ranges[1]['start']);
 106          $this->assertGreaterThan($ranges[0]['end'], $ranges[1]['start']);
 107          $this->assertGreaterThan($ranges[0]['end'], $ranges[1]['end']);
 108  
 109          $this->assertGreaterThan($ranges[1]['start'], $ranges[2]['start']);
 110          $this->assertGreaterThan($ranges[1]['end'], $ranges[2]['start']);
 111          $this->assertGreaterThan($ranges[1]['end'], $ranges[2]['end']);
 112  
 113          $this->assertGreaterThan($ranges[2]['start'], $ranges[3]['start']);
 114          $this->assertGreaterThan($ranges[2]['end'], $ranges[3]['end']);
 115          $this->assertGreaterThan($ranges[2]['end'], $ranges[3]['start']);
 116  
 117          // First range.
 118          $this->assertLessThan($nov2015, $ranges[0]['start']);
 119          $this->assertGreaterThan($nov2015, $ranges[0]['end']);
 120  
 121          // Last range.
 122          $this->assertLessThan($aug2016, $ranges[3]['start']);
 123          $this->assertGreaterThan($aug2016, $ranges[3]['end']);
 124  
 125          // Accumulative.
 126          $accum = new \core\analytics\time_splitting\quarters_accum();
 127          $accum->set_analysable($this->analysable);
 128          $ranges = $accum->get_all_ranges();
 129          $this->assertCount(4, $ranges);
 130          $this->assertCount(4, $accum->get_training_ranges());
 131          $this->assertCount(4, $accum->get_distinct_ranges());
 132  
 133          $this->assertEquals($ranges[0]['start'], $ranges[1]['start']);
 134          $this->assertEquals($ranges[1]['start'], $ranges[2]['start']);
 135          $this->assertEquals($ranges[2]['start'], $ranges[3]['start']);
 136  
 137          $this->assertGreaterThan($ranges[0]['end'], $ranges[1]['end']);
 138          $this->assertGreaterThan($ranges[1]['end'], $ranges[2]['end']);
 139          $this->assertGreaterThan($ranges[2]['end'], $ranges[3]['end']);
 140  
 141          // Present in all ranges.
 142          $this->assertLessThan($nov2015, $ranges[0]['start']);
 143          $this->assertGreaterThan($nov2015, $ranges[0]['end']);
 144          $this->assertGreaterThan($nov2015, $ranges[1]['end']);
 145          $this->assertGreaterThan($nov2015, $ranges[2]['end']);
 146          $this->assertGreaterThan($nov2015, $ranges[3]['end']);
 147  
 148          // Only in the last range.
 149          $this->assertLessThan($aug2016, $ranges[0]['end']);
 150          $this->assertLessThan($aug2016, $ranges[1]['end']);
 151          $this->assertLessThan($aug2016, $ranges[2]['end']);
 152          $this->assertLessThan($aug2016, $ranges[3]['start']);
 153          $this->assertGreaterThan($aug2016, $ranges[3]['end']);
 154      }
 155  
 156      /**
 157       * test_ready_predict
 158       *
 159       * @return void
 160       */
 161      public function test_ready_predict() {
 162  
 163          $quarters = new \core\analytics\time_splitting\quarters();
 164          $nosplitting = new \core\analytics\time_splitting\no_splitting();
 165          $singlerange = new \core\analytics\time_splitting\single_range();
 166  
 167          $range = array(
 168              'start' => time() - 100,
 169              'end' => time() - 20,
 170          );
 171          $range['time'] = $range['end'];
 172          $this->assertTrue($quarters->ready_to_predict($range));
 173          $this->assertTrue($nosplitting->ready_to_predict($range));
 174  
 175          // Single range time is 0.
 176          $range['time'] = 0;
 177          $this->assertTrue($singlerange->ready_to_predict($range));
 178  
 179          $range = array(
 180              'start' => time() + 20,
 181              'end' => time() + 100,
 182          );
 183          $range['time'] = $range['end'];
 184          $this->assertFalse($quarters->ready_to_predict($range));
 185          $this->assertTrue($nosplitting->ready_to_predict($range));
 186  
 187          // Single range time is 0.
 188          $range['time'] = 0;
 189          $this->assertTrue($singlerange->ready_to_predict($range));
 190      }
 191  
 192      /**
 193       * test_periodic
 194       *
 195       * @return void
 196       */
 197      public function test_periodic() {
 198  
 199          // Using a finished course.
 200  
 201          $pastweek = new \core\analytics\time_splitting\past_week();
 202          $pastweek->set_analysable($this->analysable);
 203          $this->assertCount(1, $pastweek->get_distinct_ranges());
 204  
 205          $ranges = $pastweek->get_all_ranges();
 206          $this->assertEquals(52, count($ranges));
 207          $this->assertEquals($this->course->startdate, $ranges[0]['start']);
 208          $this->assertNotEquals($this->course->startdate, $ranges[0]['time']);
 209  
 210          // The analysable is finished so all ranges are available for training.
 211          $this->assertCount(count($ranges), $pastweek->get_training_ranges());
 212  
 213          $ranges = $pastweek->get_most_recent_prediction_range();
 214          $range = reset($ranges);
 215          $this->assertEquals(51, key($ranges));
 216  
 217          // We now use an ongoing course not yet ready to generate predictions.
 218  
 219          $threedaysago = new \DateTime('-3 days');
 220          $params = array(
 221              'startdate' => $threedaysago->getTimestamp(),
 222          );
 223          $ongoingcourse = $this->getDataGenerator()->create_course($params);
 224          $ongoinganalysable = new \core_analytics\course($ongoingcourse);
 225  
 226          $pastweek = new \core\analytics\time_splitting\past_week();
 227          $pastweek->set_analysable($ongoinganalysable);
 228          $ranges = $pastweek->get_all_ranges();
 229          $this->assertEquals(0, count($ranges));
 230          $this->assertCount(0, $pastweek->get_training_ranges());
 231  
 232          // We now use a ready-to-predict ongoing course.
 233  
 234          $onemonthago = new \DateTime('-30 days');
 235          $params = array(
 236              'startdate' => $onemonthago->getTimestamp(),
 237          );
 238          $ongoingcourse = $this->getDataGenerator()->create_course($params);
 239          $ongoinganalysable = new \core_analytics\course($ongoingcourse);
 240  
 241          $pastweek = new \core\analytics\time_splitting\past_week();
 242          $pastweek->set_analysable($ongoinganalysable);
 243          $this->assertCount(1, $pastweek->get_distinct_ranges());
 244  
 245          $ranges = $pastweek->get_all_ranges();
 246          $this->assertEquals(4, count($ranges));
 247          $this->assertCount(4, $pastweek->get_training_ranges());
 248  
 249          $ranges = $pastweek->get_most_recent_prediction_range();
 250          $range = reset($ranges);
 251          $this->assertEquals(3, key($ranges));
 252          $this->assertEqualsWithDelta(time(), $range['time'], 1);
 253          // 1 second delta for the start just in case a second passes between the set_analysable call
 254          // and this checking below.
 255          $time = new \DateTime();
 256          $time->sub($pastweek->periodicity());
 257          $this->assertEqualsWithDelta($time->getTimestamp(), $range['start'], 1.0);
 258          $this->assertEqualsWithDelta(time(), $range['end'], 1);
 259  
 260          $starttime = time();
 261  
 262          $upcomingweek = new \core\analytics\time_splitting\upcoming_week();
 263          $upcomingweek->set_analysable($ongoinganalysable);
 264          $this->assertCount(1, $upcomingweek->get_distinct_ranges());
 265  
 266          $ranges = $upcomingweek->get_all_ranges();
 267          $this->assertEquals(1, count($ranges));
 268          $range = reset($ranges);
 269          $this->assertEqualsWithDelta(time(), $range['time'], 1);
 270          $this->assertEqualsWithDelta(time(), $range['start'], 1);
 271          $this->assertGreaterThan(time(), $range['end']);
 272  
 273          $this->assertCount(0, $upcomingweek->get_training_ranges());
 274  
 275          $ranges = $upcomingweek->get_most_recent_prediction_range();
 276          $range = reset($ranges);
 277          $this->assertEquals(0, key($ranges));
 278          $this->assertEqualsWithDelta(time(), $range['time'], 1);
 279          $this->assertEqualsWithDelta(time(), $range['start'], 1);
 280          $this->assertGreaterThanOrEqual($starttime, $range['time']);
 281          $this->assertGreaterThanOrEqual($starttime, $range['start']);
 282          $this->assertGreaterThan(time(), $range['end']);
 283  
 284          $this->assertNotEmpty($upcomingweek->get_range_by_index(0));
 285          $this->assertFalse($upcomingweek->get_range_by_index(1));
 286  
 287          // We now check how new ranges get added as time passes.
 288  
 289          $fewsecsago = new \DateTime('-5 seconds');
 290          $params = array(
 291              'startdate' => $fewsecsago->getTimestamp(),
 292              'enddate' => (new \DateTimeImmutable('+1 year'))->getTimestamp(),
 293          );
 294          $course = $this->getDataGenerator()->create_course($params);
 295          $analysable = new \core_analytics\course($course);
 296  
 297          $seconds = new test_timesplitting_seconds();
 298          $seconds->set_analysable($analysable);
 299  
 300          // Store the ranges we just obtained.
 301          $ranges = $seconds->get_all_ranges();
 302          $nranges = count($ranges);
 303          $ntrainingranges = count($seconds->get_training_ranges());
 304          $mostrecentrange = $seconds->get_most_recent_prediction_range();
 305          $mostrecentrange = reset($mostrecentrange);
 306  
 307          // We wait for the next range to be added.
 308          sleep(1);
 309  
 310          // We set the analysable again so the time ranges are recalculated.
 311          $seconds->set_analysable($analysable);
 312  
 313          $newranges = $seconds->get_all_ranges();
 314          $nnewranges = count($newranges);
 315          $nnewtrainingranges = $seconds->get_training_ranges();
 316          $newmostrecentrange = $seconds->get_most_recent_prediction_range();
 317          $newmostrecentrange = reset($newmostrecentrange);
 318          $this->assertGreaterThan($nranges, $nnewranges);
 319          $this->assertGreaterThan($ntrainingranges, $nnewtrainingranges);
 320          $this->assertGreaterThan($mostrecentrange['time'], $newmostrecentrange['time']);
 321  
 322          // All the ranges but the last one should return the same values.
 323          array_pop($ranges);
 324          array_pop($newranges);
 325          foreach ($ranges as $key => $range) {
 326              $this->assertEquals($newranges[$key]['start'], $range['start']);
 327              $this->assertEquals($newranges[$key]['end'], $range['end']);
 328              $this->assertEquals($newranges[$key]['time'], $range['time']);
 329          }
 330  
 331          // Fake model id, we can use any int, we will need to reference it later.
 332          $modelid = 1505347200;
 333  
 334          $upcomingseconds = new test_timesplitting_upcoming_seconds();
 335          $upcomingseconds->set_modelid($modelid);
 336          $upcomingseconds->set_analysable($analysable);
 337  
 338          // Store the ranges we just obtained.
 339          $ranges = $upcomingseconds->get_all_ranges();
 340          $nranges = count($ranges);
 341          $ntrainingranges = count($upcomingseconds->get_training_ranges());
 342          $mostrecentrange = $upcomingseconds->get_most_recent_prediction_range();
 343          $mostrecentrange = reset($mostrecentrange);
 344  
 345          // Mimic the modelfirstanalyses caching in \core_analytics\analysis.
 346          $this->mock_cache_first_analysis_caching($modelid, $analysable->get_id(), end($ranges));
 347  
 348          // We wait for the next range to be added.
 349          sleep(1);
 350  
 351          // We set the analysable again so the time ranges are recalculated.
 352          $upcomingseconds->set_analysable($analysable);
 353  
 354          $newranges = $upcomingseconds->get_all_ranges();
 355          $nnewranges = count($newranges);
 356          $nnewtrainingranges = $upcomingseconds->get_training_ranges();
 357          $newmostrecentrange = $upcomingseconds->get_most_recent_prediction_range();
 358          $newmostrecentrange = reset($newmostrecentrange);
 359          $this->assertGreaterThan($nranges, $nnewranges);
 360          $this->assertGreaterThan($ntrainingranges, $nnewtrainingranges);
 361          $this->assertGreaterThan($mostrecentrange['time'], $newmostrecentrange['time']);
 362  
 363          // All the ranges but the last one should return the same values.
 364          array_pop($ranges);
 365          array_pop($newranges);
 366          foreach ($ranges as $key => $range) {
 367              $this->assertEquals($newranges[$key]['start'], $range['start']);
 368              $this->assertEquals($newranges[$key]['end'], $range['end']);
 369              $this->assertEquals($newranges[$key]['time'], $range['time']);
 370          }
 371      }
 372  
 373      /**
 374       * Mocks core_analytics\analysis caching of the first time analysables were analysed.
 375       *
 376       * @param  int $modelid
 377       * @param  int $analysableid
 378       * @param  array $range
 379       * @return null
 380       */
 381      private function mock_cache_first_analysis_caching($modelid, $analysableid, $range) {
 382          $cache = \cache::make('core', 'modelfirstanalyses');
 383          $cache->set($modelid . '_' . $analysableid, $range['time']);
 384      }
 385  }