Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310]

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