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.

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