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 311 and 401] [Versions 400 and 401] [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\task;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  require_once (__DIR__ . '/../fixtures/task_fixtures.php');
  21  
  22  /**
  23   * Test class for scheduled task.
  24   *
  25   * @package core
  26   * @category test
  27   * @copyright 2013 Damyon Wiese
  28   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  29   * @coversDefaultClass \core\task\scheduled_task
  30   */
  31  class scheduled_task_test extends \advanced_testcase {
  32  
  33      /**
  34       * Data provider for {@see test_eval_cron_field}
  35       *
  36       * @return array
  37       */
  38      public static function eval_cron_provider(): array {
  39          return [
  40              // At every 3rd <unit>.
  41              ['*/3', 0, 29, [0, 3, 6, 9, 12, 15, 18, 21, 24, 27]],
  42              // At <unit> 1 and every 2nd <unit>.
  43              ['1,*/2', 0, 29, [0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28]],
  44              // At every <unit> from 1 through 10 and every <unit> from 5 through 15.
  45              ['1-10,5-15', 0, 29, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]],
  46              // At every <unit> from 1 through 10 and every 2nd <unit> from 5 through 15.
  47              ['1-10,5-15/2', 0, 29, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15]],
  48              // At every <unit> from 1 through 10 and every 2nd <unit> from 5 through 29.
  49              ['1-10,5/2', 0, 29, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29]],
  50              // At <unit> 1, 2, 3.
  51              ['1,2,3,1,2,3', 0, 29, [1, 2, 3]],
  52              // Invalid.
  53              ['-1,10,80', 0, 29, []],
  54              // Invalid.
  55              ['-1', 0, 29, []],
  56          ];
  57      }
  58  
  59      /**
  60       * Test the cron scheduling method
  61       *
  62       * @param string $field
  63       * @param int $min
  64       * @param int $max
  65       * @param int[] $expected
  66       *
  67       * @dataProvider eval_cron_provider
  68       *
  69       * @covers ::eval_cron_field
  70       */
  71      public function test_eval_cron_field(string $field, int $min, int $max, array $expected): void {
  72          $testclass = new scheduled_test_task();
  73  
  74          $this->assertEquals($expected, $testclass->eval_cron_field($field, $min, $max));
  75      }
  76  
  77      public function test_get_next_scheduled_time() {
  78          global $CFG;
  79          $this->resetAfterTest();
  80  
  81          $this->setTimezone('Europe/London');
  82  
  83          // Let's specify the hour we are going to use initially for the test.
  84          // (note that we pick 01:00 that is tricky for Europe/London, because
  85          // it's exactly the Daylight Saving Time Begins hour.
  86          $testhour = 1;
  87  
  88          // Test job run at 1 am.
  89          $testclass = new scheduled_test_task();
  90  
  91          // All fields default to '*'.
  92          $testclass->set_hour($testhour);
  93          $testclass->set_minute('0');
  94          // Next valid time should be 1am of the next day.
  95          $nexttime = $testclass->get_next_scheduled_time();
  96  
  97          $oneamdate = new \DateTime('now', new \DateTimeZone('Europe/London'));
  98          $oneamdate->setTime($testhour, 0, 0);
  99  
 100          // Once a year (currently last Sunday of March), when changing to Daylight Saving Time,
 101          // Europe/London 01:00 simply doesn't exists because, exactly at 01:00 the clock
 102          // is advanced by one hour and becomes 02:00. When that happens, the DateInterval
 103          // calculations cannot be to advance by 1 day, but by one less hour. That is exactly when
 104          // the next scheduled run will happen (next day 01:00).
 105          $isdaylightsaving = false;
 106          if ($testhour < (int)$oneamdate->format('H')) {
 107              $isdaylightsaving = true;
 108          }
 109  
 110          // Make it 1 am tomorrow if the time is after 1am.
 111          if ($oneamdate->getTimestamp() < time()) {
 112              $oneamdate->add(new \DateInterval('P1D'));
 113              if ($isdaylightsaving) {
 114                  // If today is Europe/London Daylight Saving Time Begins, expectation is 1 less hour.
 115                  $oneamdate->sub(new \DateInterval('PT1H'));
 116              }
 117          }
 118          $oneam = $oneamdate->getTimestamp();
 119  
 120          $this->assertEquals($oneam, $nexttime, 'Next scheduled time is 1am.');
 121  
 122          // Disabled flag does not affect next time.
 123          $testclass->set_disabled(true);
 124          $nexttime = $testclass->get_next_scheduled_time();
 125          $this->assertEquals($oneam, $nexttime, 'Next scheduled time is 1am.');
 126  
 127          // Now test for job run every 10 minutes.
 128          $testclass = new scheduled_test_task();
 129  
 130          // All fields default to '*'.
 131          $testclass->set_minute('*/10');
 132          // Next valid time should be next 10 minute boundary.
 133          $nexttime = $testclass->get_next_scheduled_time();
 134  
 135          $minutes = ((intval(date('i') / 10)) + 1) * 10;
 136          $nexttenminutes = mktime(date('H'), $minutes, 0);
 137  
 138          $this->assertEquals($nexttenminutes, $nexttime, 'Next scheduled time is in 10 minutes.');
 139  
 140          // Disabled flag does not affect next time.
 141          $testclass->set_disabled(true);
 142          $nexttime = $testclass->get_next_scheduled_time();
 143          $this->assertEquals($nexttenminutes, $nexttime, 'Next scheduled time is in 10 minutes.');
 144  
 145          // Test hourly job executed on Sundays only.
 146          $testclass = new scheduled_test_task();
 147          $testclass->set_minute('0');
 148          $testclass->set_day_of_week('7');
 149  
 150          $nexttime = $testclass->get_next_scheduled_time();
 151  
 152          $this->assertEquals(7, date('N', $nexttime));
 153          $this->assertEquals(0, date('i', $nexttime));
 154  
 155          // Test monthly job.
 156          $testclass = new scheduled_test_task();
 157          $testclass->set_minute('32');
 158          $testclass->set_hour('0');
 159          $testclass->set_day('1');
 160  
 161          $nexttime = $testclass->get_next_scheduled_time();
 162  
 163          $this->assertEquals(32, date('i', $nexttime));
 164          $this->assertEquals(0, date('G', $nexttime));
 165          $this->assertEquals(1, date('j', $nexttime));
 166      }
 167  
 168      /**
 169       * Data provider for get_next_scheduled_time_detail.
 170       *
 171       * Note all times in here are in default Australia/Perth time zone.
 172       *
 173       * @return array[] Function parameters for each run
 174       */
 175      public static function get_next_scheduled_time_detail_provider(): array {
 176          return [
 177              // Every minute = next minute.
 178              ['2023-11-01 15:15', '*', '*', '*', '*', '*', '2023-11-01 15:16'],
 179              // Specified minute (coming up) = same hour, that minute.
 180              ['2023-11-01 15:15', '18', '*', '*', '*', '*', '2023-11-01 15:18'],
 181              // Specified minute (passed) = next hour, that minute.
 182              ['2023-11-01 15:15', '11', '*', '*', '*', '*', '2023-11-01 16:11'],
 183              // Range of minutes = same hour, next matching value.
 184              ['2023-11-01 15:15', '*/15', '*', '*', '*', '*', '2023-11-01 15:30'],
 185              // Specified hour, any minute = first minute that hour.
 186              ['2023-11-01 15:15', '*', '20', '*', '*', '*', '2023-11-01 20:00'],
 187              // Specified hour, specified minute = that time.
 188              ['2023-11-01 15:15', '13', '20', '*', '*', '*', '2023-11-01 20:13'],
 189              // Any minute, range of hours = next hour in range, 00:00.
 190              ['2023-11-01 15:15', '*', '*/6', '*', '*', '*', '2023-11-01 18:00'],
 191              // Specified minute, range of hours = next hour where minute not passed, that minute.
 192              ['2023-11-01 18:15', '10', '*/6', '*', '*', '*', '2023-11-02 00:10'],
 193              // Specified day, any hour/minute.
 194              ['2023-11-01 15:15', '*', '*', '3', '*', '*', '2023-11-03 00:00'],
 195              // Specified day (next month), any hour/minute.
 196              ['2023-11-05 15:15', '*', '*', '3', '*', '*', '2023-12-03 00:00'],
 197              // Specified day, specified hour.
 198              ['2023-11-01 15:15', '*', '17', '3', '*', '*', '2023-11-03 17:00'],
 199              // Specified day, specified minute.
 200              ['2023-11-01 15:15', '17', '*', '3', '*', '*', '2023-11-03 00:17'],
 201              // 30th of every month, February.
 202              ['2023-01-31 15:15', '15', '10', '30', '*', '*', '2023-03-30 10:15'],
 203              // Friday, any time. 2023-11-01 is a Wednesday, so it will run in 2 days.
 204              ['2023-11-01 15:15', '*', '*', '*', '5', '*', '2023-11-03 00:00'],
 205              // Friday, any time (but it's already Friday).
 206              ['2023-11-03 15:15', '*', '*', '*', '5', '*', '2023-11-03 15:16'],
 207              // Sunday (week rollover).
 208              ['2023-11-01 15:15', '*', '*', '*', '0', '*', '2023-11-05 00:00'],
 209              // Specified days and day of week (days come first).
 210              ['2023-11-01 15:15', '*', '*', '2,4,6', '5', '*', '2023-11-02 00:00'],
 211              // Specified days and day of week (day of week comes first).
 212              ['2023-11-01 15:15', '*', '*', '4,6,8', '5', '*', '2023-11-03 00:00'],
 213              // Specified months.
 214              ['2023-11-01 15:15', '*', '*', '*', '*', '6,8,10,12', '2023-12-01 00:00'],
 215              // Specified months (crossing year).
 216              ['2023-11-01 15:15', '*', '*', '*', '*', '6,8,10', '2024-06-01 00:00'],
 217              // Specified months and day of week (i.e. first Sunday in December).
 218              ['2023-11-01 15:15', '*', '*', '*', '0', '6,8,10,12', '2023-12-03 00:00'],
 219              // It's already December, but the next Friday is not until next month.
 220              ['2023-12-30 15:15', '*', '*', '*', '5', '6,8,10,12', '2024-06-07 00:00'],
 221              // Around end of year.
 222              ['2023-12-31 23:00', '10', '3', '*', '*', '*', '2024-01-01 03:10'],
 223              // Some impossible requirements...
 224              ['2023-12-31 23:00', '*', '*', '30', '*', '2', scheduled_task::NEVER_RUN_TIME],
 225              ['2023-12-31 23:00', '*', '*', '31', '*', '9,4,6,11', scheduled_task::NEVER_RUN_TIME],
 226              // Normal years and leap years.
 227              ['2021-01-01 23:00', '*', '*', '28', '*', '2', '2021-02-28 00:00'],
 228              ['2021-01-01 23:00', '*', '*', '29', '*', '2', '2024-02-29 00:00'],
 229              // Missing leap year over century. Longest possible gap between runs.
 230              ['2096-03-01 00:00', '59', '23', '29', '*', '2', '2104-02-29 23:59'],
 231          ];
 232      }
 233  
 234      /**
 235       * Tests get_next_scheduled_time using a large number of example scenarios.
 236       *
 237       * @param string $now Current time (strtotime format)
 238       * @param string $minute Minute restriction list for task
 239       * @param string $hour Hour restriction list for task
 240       * @param string $day Day restriction list for task
 241       * @param string $dayofweek Day of week restriction list for task
 242       * @param string $month Month restriction list for task
 243       * @param string|int $expected Expected run time (strtotime format or time int)
 244       * @dataProvider get_next_scheduled_time_detail_provider
 245       * @covers ::get_next_scheduled_time
 246       */
 247      public function test_get_next_scheduled_time_detail(string $now, string $minute, string $hour,
 248              string $day, string $dayofweek, string $month, $expected): void {
 249          // Create test task with specified times.
 250          $task = new scheduled_test_task();
 251          $task->set_minute($minute);
 252          $task->set_hour($hour);
 253          $task->set_day($day);
 254          $task->set_day_of_week($dayofweek);
 255          $task->set_month($month);
 256  
 257          // Check function results.
 258          $nowtime = strtotime($now);
 259          if (is_int($expected)) {
 260              $expectedtime = $expected;
 261          } else {
 262              $expectedtime = strtotime($expected);
 263          }
 264          $actualtime = $task->get_next_scheduled_time($nowtime);
 265          $this->assertEquals($expectedtime, $actualtime, 'Expected ' . $expected . ', actual ' . date('Y-m-d H:i', $actualtime));
 266      }
 267  
 268      /**
 269       * Tests get_next_scheduled_time around DST changes, with regard to the continuity of frequent
 270       * tasks.
 271       *
 272       * We want frequent tasks to keep progressing as normal and not randomly stop for an hour, or
 273       * suddenly decide they need to happen in the past.
 274       *
 275       * @covers ::get_next_scheduled_time
 276       */
 277      public function test_get_next_scheduled_time_dst_continuity(): void {
 278          $this->resetAfterTest();
 279          $this->setTimezone('Europe/London');
 280  
 281          // Test task is set to run every 20 minutes (:00, :20, :40).
 282          $task = new scheduled_test_task();
 283          $task->set_minute('*/20');
 284  
 285          // DST change forwards. Check times in GMT to ensure it progresses as normal.
 286          $before = strtotime('2023-03-26 00:59 GMT');
 287          $this->assertEquals(strtotime('2023-03-26 00:59 Europe/London'), $before);
 288          $one = $task->get_next_scheduled_time($before);
 289          $this->assertEquals(strtotime('2023-03-26 01:00 GMT'), $one);
 290          $this->assertEquals(strtotime('2023-03-26 02:00 Europe/London'), $one);
 291          $two = $task->get_next_scheduled_time($one);
 292          $this->assertEquals(strtotime('2023-03-26 01:20 GMT'), $two);
 293          $three = $task->get_next_scheduled_time($two);
 294          $this->assertEquals(strtotime('2023-03-26 01:40 GMT'), $three);
 295          $four = $task->get_next_scheduled_time($three);
 296          $this->assertEquals(strtotime('2023-03-26 02:00 GMT'), $four);
 297  
 298          // DST change backwards.
 299          $before = strtotime('2023-10-29 00:59 GMT');
 300          // The 'before' time is 01:59 Europe/London, but we won't explicitly test that because
 301          // there are two 01:59s so it might fail depending on implementation.
 302          $one = $task->get_next_scheduled_time($before);
 303          $this->assertEquals(strtotime('2023-10-29 01:00 GMT'), $one);
 304          // We cannot compare against the Eerope/London time (01:00) because there are two 01:00s.
 305          $two = $task->get_next_scheduled_time($one);
 306          $this->assertEquals(strtotime('2023-10-29 01:20 GMT'), $two);
 307          $three = $task->get_next_scheduled_time($two);
 308          $this->assertEquals(strtotime('2023-10-29 01:40 GMT'), $three);
 309          $four = $task->get_next_scheduled_time($three);
 310          $this->assertEquals(strtotime('2023-10-29 02:00 GMT'), $four);
 311          // This time is now unambiguous in Europe/London.
 312          $this->assertEquals(strtotime('2023-10-29 02:00 Europe/London'), $four);
 313      }
 314  
 315      public function test_timezones() {
 316          global $CFG, $USER;
 317  
 318          // The timezones used in this test are chosen because they do not use DST - that would break the test.
 319          $this->resetAfterTest();
 320  
 321          $this->setTimezone('Asia/Kabul');
 322  
 323          $testclass = new scheduled_test_task();
 324  
 325          // Scheduled tasks should always use servertime - so this is 03:30 GMT.
 326          $testclass->set_hour('1');
 327          $testclass->set_minute('0');
 328  
 329          // Next valid time should be 1am of the next day.
 330          $nexttime = $testclass->get_next_scheduled_time();
 331  
 332          // GMT+05:45.
 333          $USER->timezone = 'Asia/Kathmandu';
 334          $userdate = userdate($nexttime);
 335  
 336          // Should be displayed in user timezone.
 337          // I used http://www.timeanddate.com/worldclock/fixedtime.html?msg=Moodle+Test&iso=20160502T01&p1=113
 338          // setting my location to Kathmandu to verify this time.
 339          $this->assertStringContainsString('2:15 AM', \core_text::strtoupper($userdate));
 340      }
 341  
 342      public function test_reset_scheduled_tasks_for_component_customised(): void {
 343          $this->resetAfterTest(true);
 344  
 345          $tasks = manager::load_scheduled_tasks_for_component('moodle');
 346  
 347          // Customise a task.
 348          $task = reset($tasks);
 349          $task->set_minute('1');
 350          $task->set_hour('2');
 351          $task->set_day('3');
 352          $task->set_month('4');
 353          $task->set_day_of_week('5');
 354          $task->set_customised('1');
 355          manager::configure_scheduled_task($task);
 356  
 357          // Now call reset.
 358          manager::reset_scheduled_tasks_for_component('moodle');
 359  
 360          // Fetch the task again.
 361          $taskafterreset = manager::get_scheduled_task(get_class($task));
 362  
 363          // The task should still be the same as the customised.
 364          $this->assertTaskEquals($task, $taskafterreset);
 365      }
 366  
 367      public function test_reset_scheduled_tasks_for_component_deleted(): void {
 368          global $DB;
 369          $this->resetAfterTest(true);
 370  
 371          // Delete a task to simulate the fact that its new.
 372          $tasklist = manager::load_scheduled_tasks_for_component('moodle');
 373  
 374          // Note: This test must use a task which does not use any random values.
 375          $task = manager::get_scheduled_task(session_cleanup_task::class);
 376  
 377          $DB->delete_records('task_scheduled', array('classname' => '\\' . trim(get_class($task), '\\')));
 378          $this->assertFalse(manager::get_scheduled_task(session_cleanup_task::class));
 379  
 380          // Now call reset on all the tasks.
 381          manager::reset_scheduled_tasks_for_component('moodle');
 382  
 383          // Assert that the second task was added back.
 384          $taskafterreset = manager::get_scheduled_task(session_cleanup_task::class);
 385          $this->assertNotFalse($taskafterreset);
 386  
 387          $this->assertTaskEquals($task, $taskafterreset);
 388          $this->assertCount(count($tasklist), manager::load_scheduled_tasks_for_component('moodle'));
 389      }
 390  
 391      public function test_reset_scheduled_tasks_for_component_changed_in_source(): void {
 392          $this->resetAfterTest(true);
 393  
 394          // Delete a task to simulate the fact that its new.
 395          // Note: This test must use a task which does not use any random values.
 396          $task = manager::get_scheduled_task(session_cleanup_task::class);
 397  
 398          // Get a copy of the task before maing changes for later comparison.
 399          $taskbeforechange = manager::get_scheduled_task(session_cleanup_task::class);
 400  
 401          // Edit a task to simulate a change in its definition (as if it was not customised).
 402          $task->set_minute('1');
 403          $task->set_hour('2');
 404          $task->set_day('3');
 405          $task->set_month('4');
 406          $task->set_day_of_week('5');
 407          manager::configure_scheduled_task($task);
 408  
 409          // Fetch the task out for comparison.
 410          $taskafterchange = manager::get_scheduled_task(session_cleanup_task::class);
 411  
 412          // The task should now be different to the original.
 413          $this->assertTaskNotEquals($taskbeforechange, $taskafterchange);
 414  
 415          // Now call reset.
 416          manager::reset_scheduled_tasks_for_component('moodle');
 417  
 418          // Fetch the task again.
 419          $taskafterreset = manager::get_scheduled_task(session_cleanup_task::class);
 420  
 421          // The task should now be the same as the original.
 422          $this->assertTaskEquals($taskbeforechange, $taskafterreset);
 423      }
 424  
 425      /**
 426       * Tests that the reset function deletes old tasks.
 427       */
 428      public function test_reset_scheduled_tasks_for_component_delete() {
 429          global $DB;
 430          $this->resetAfterTest(true);
 431  
 432          $count = $DB->count_records('task_scheduled', array('component' => 'moodle'));
 433          $allcount = $DB->count_records('task_scheduled');
 434  
 435          $task = new scheduled_test_task();
 436          $task->set_component('moodle');
 437          $record = manager::record_from_scheduled_task($task);
 438          $DB->insert_record('task_scheduled', $record);
 439          $this->assertTrue($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test_task',
 440              'component' => 'moodle')));
 441  
 442          $task = new scheduled_test2_task();
 443          $task->set_component('moodle');
 444          $record = manager::record_from_scheduled_task($task);
 445          $DB->insert_record('task_scheduled', $record);
 446          $this->assertTrue($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test2_task',
 447              'component' => 'moodle')));
 448  
 449          $aftercount = $DB->count_records('task_scheduled', array('component' => 'moodle'));
 450          $afterallcount = $DB->count_records('task_scheduled');
 451  
 452          $this->assertEquals($count + 2, $aftercount);
 453          $this->assertEquals($allcount + 2, $afterallcount);
 454  
 455          // Now check that the right things were deleted.
 456          manager::reset_scheduled_tasks_for_component('moodle');
 457  
 458          $this->assertEquals($count, $DB->count_records('task_scheduled', array('component' => 'moodle')));
 459          $this->assertEquals($allcount, $DB->count_records('task_scheduled'));
 460          $this->assertFalse($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test2_task',
 461              'component' => 'moodle')));
 462          $this->assertFalse($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test_task',
 463              'component' => 'moodle')));
 464      }
 465  
 466      public function test_get_next_scheduled_task() {
 467          global $DB;
 468  
 469          $this->resetAfterTest(true);
 470          // Delete all existing scheduled tasks.
 471          $DB->delete_records('task_scheduled');
 472          // Add a scheduled task.
 473  
 474          // A task that runs once per hour.
 475          $record = new \stdClass();
 476          $record->blocking = true;
 477          $record->minute = '0';
 478          $record->hour = '0';
 479          $record->dayofweek = '*';
 480          $record->day = '*';
 481          $record->month = '*';
 482          $record->component = 'test_scheduled_task';
 483          $record->classname = '\core\task\scheduled_test_task';
 484  
 485          $DB->insert_record('task_scheduled', $record);
 486          // And another one to test failures.
 487          $record->classname = '\core\task\scheduled_test2_task';
 488          $DB->insert_record('task_scheduled', $record);
 489          // And disabled test.
 490          $record->classname = '\core\task\scheduled_test3_task';
 491          $record->disabled = 1;
 492          $DB->insert_record('task_scheduled', $record);
 493  
 494          $now = time();
 495  
 496          // Should get handed the first task.
 497          $task = manager::get_next_scheduled_task($now);
 498          $this->assertInstanceOf('\core\task\scheduled_test_task', $task);
 499          $task->execute();
 500  
 501          manager::scheduled_task_complete($task);
 502          // Should get handed the second task.
 503          $task = manager::get_next_scheduled_task($now);
 504          $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
 505          $task->execute();
 506  
 507          manager::scheduled_task_failed($task);
 508          // Should not get any task.
 509          $task = manager::get_next_scheduled_task($now);
 510          $this->assertNull($task);
 511  
 512          // Should get the second task (retry after delay).
 513          $task = manager::get_next_scheduled_task($now + 120);
 514          $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
 515          $task->execute();
 516  
 517          manager::scheduled_task_complete($task);
 518  
 519          // Should not get any task.
 520          $task = manager::get_next_scheduled_task($now);
 521          $this->assertNull($task);
 522  
 523          // Check ordering.
 524          $DB->delete_records('task_scheduled');
 525          $record->lastruntime = 2;
 526          $record->disabled = 0;
 527          $record->classname = '\core\task\scheduled_test_task';
 528          $DB->insert_record('task_scheduled', $record);
 529  
 530          $record->lastruntime = 1;
 531          $record->classname = '\core\task\scheduled_test2_task';
 532          $DB->insert_record('task_scheduled', $record);
 533  
 534          // Should get handed the second task.
 535          $task = manager::get_next_scheduled_task($now);
 536          $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
 537          $task->execute();
 538          manager::scheduled_task_complete($task);
 539  
 540          // Should get handed the first task.
 541          $task = manager::get_next_scheduled_task($now);
 542          $this->assertInstanceOf('\core\task\scheduled_test_task', $task);
 543          $task->execute();
 544          manager::scheduled_task_complete($task);
 545  
 546          // Should not get any task.
 547          $task = manager::get_next_scheduled_task($now);
 548          $this->assertNull($task);
 549      }
 550  
 551      public function test_get_broken_scheduled_task() {
 552          global $DB;
 553  
 554          $this->resetAfterTest(true);
 555          // Delete all existing scheduled tasks.
 556          $DB->delete_records('task_scheduled');
 557          // Add a scheduled task.
 558  
 559          // A broken task that runs all the time.
 560          $record = new \stdClass();
 561          $record->blocking = true;
 562          $record->minute = '*';
 563          $record->hour = '*';
 564          $record->dayofweek = '*';
 565          $record->day = '*';
 566          $record->month = '*';
 567          $record->component = 'test_scheduled_task';
 568          $record->classname = '\core\task\scheduled_test_task_broken';
 569  
 570          $DB->insert_record('task_scheduled', $record);
 571  
 572          $now = time();
 573          // Should not get any task.
 574          $task = manager::get_next_scheduled_task($now);
 575          $this->assertDebuggingCalled();
 576          $this->assertNull($task);
 577      }
 578  
 579      /**
 580       * Tests the use of 'R' syntax in time fields of tasks to get
 581       * tasks be configured with a non-uniform time.
 582       */
 583      public function test_random_time_specification() {
 584  
 585          // Testing non-deterministic things in a unit test is not really
 586          // wise, so we just test the values have changed within allowed bounds.
 587          $testclass = new scheduled_test_task();
 588  
 589          // The test task defaults to '*'.
 590          $this->assertIsString($testclass->get_minute());
 591          $this->assertIsString($testclass->get_hour());
 592  
 593          // Set a random value.
 594          $testclass->set_minute('R');
 595          $testclass->set_hour('R');
 596          $testclass->set_day_of_week('R');
 597  
 598          // Verify the minute has changed within allowed bounds.
 599          $minute = $testclass->get_minute();
 600          $this->assertIsInt($minute);
 601          $this->assertGreaterThanOrEqual(0, $minute);
 602          $this->assertLessThanOrEqual(59, $minute);
 603  
 604          // Verify the hour has changed within allowed bounds.
 605          $hour = $testclass->get_hour();
 606          $this->assertIsInt($hour);
 607          $this->assertGreaterThanOrEqual(0, $hour);
 608          $this->assertLessThanOrEqual(23, $hour);
 609  
 610          // Verify the dayofweek has changed within allowed bounds.
 611          $dayofweek = $testclass->get_day_of_week();
 612          $this->assertIsInt($dayofweek);
 613          $this->assertGreaterThanOrEqual(0, $dayofweek);
 614          $this->assertLessThanOrEqual(6, $dayofweek);
 615      }
 616  
 617      /**
 618       * Test that the file_temp_cleanup_task removes directories and
 619       * files as expected.
 620       */
 621      public function test_file_temp_cleanup_task() {
 622          global $CFG;
 623          $backuptempdir = make_backup_temp_directory('');
 624  
 625          // Create directories.
 626          $dir = $backuptempdir . DIRECTORY_SEPARATOR . 'backup01' . DIRECTORY_SEPARATOR . 'courses';
 627          mkdir($dir, 0777, true);
 628  
 629          // Create files to be checked and then deleted.
 630          $file01 = $dir . DIRECTORY_SEPARATOR . 'sections.xml';
 631          file_put_contents($file01, 'test data 001');
 632          $file02 = $dir . DIRECTORY_SEPARATOR . 'modules.xml';
 633          file_put_contents($file02, 'test data 002');
 634          // Change the time modified for the first file, to a time that will be deleted by the task (greater than seven days).
 635          touch($file01, time() - (8 * 24 * 3600));
 636  
 637          $task = manager::get_scheduled_task('\\core\\task\\file_temp_cleanup_task');
 638          $this->assertInstanceOf('\core\task\file_temp_cleanup_task', $task);
 639          $task->execute();
 640  
 641          // Scan the directory. Only modules.xml should be left.
 642          $filesarray = scandir($dir);
 643          $this->assertEquals('modules.xml', $filesarray[2]);
 644          $this->assertEquals(3, count($filesarray));
 645  
 646          // Change the time modified on modules.xml.
 647          touch($file02, time() - (8 * 24 * 3600));
 648          // Change the time modified on the courses directory.
 649          touch($backuptempdir . DIRECTORY_SEPARATOR . 'backup01' . DIRECTORY_SEPARATOR .
 650                  'courses', time() - (8 * 24 * 3600));
 651          // Run the scheduled task to remove the file and directory.
 652          $task->execute();
 653          $filesarray = scandir($backuptempdir . DIRECTORY_SEPARATOR . 'backup01');
 654          // There should only be two items in the array, '.' and '..'.
 655          $this->assertEquals(2, count($filesarray));
 656  
 657          // Change the time modified on all of the files and directories.
 658          $dir = new \RecursiveDirectoryIterator($CFG->tempdir);
 659          // Show all child nodes prior to their parent.
 660          $iter = new \RecursiveIteratorIterator($dir, \RecursiveIteratorIterator::CHILD_FIRST);
 661  
 662          for ($iter->rewind(); $iter->valid(); $iter->next()) {
 663              if ($iter->isDir() && !$iter->isDot()) {
 664                  $node = $iter->getRealPath();
 665                  touch($node, time() - (8 * 24 * 3600));
 666              }
 667          }
 668  
 669          // Run the scheduled task again to remove all of the files and directories.
 670          $task->execute();
 671          $filesarray = scandir($CFG->tempdir);
 672          // All of the files and directories should be deleted.
 673          // There should only be three items in the array, '.', '..' and '.htaccess'.
 674          $this->assertEquals([ '.', '..', '.htaccess' ], $filesarray);
 675      }
 676  
 677      /**
 678       * Test that the function to clear the fail delay from a task works correctly.
 679       */
 680      public function test_clear_fail_delay() {
 681  
 682          $this->resetAfterTest();
 683  
 684          // Get an example task to use for testing. Task is set to run every minute by default.
 685          $taskname = '\core\task\send_new_user_passwords_task';
 686  
 687          // Pretend task started running and then failed 3 times.
 688          $before = time();
 689          $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
 690          for ($i = 0; $i < 3; $i ++) {
 691              $task = manager::get_scheduled_task($taskname);
 692              $lock = $cronlockfactory->get_lock('\\' . get_class($task), 10);
 693              $task->set_lock($lock);
 694              manager::scheduled_task_failed($task);
 695          }
 696  
 697          // Confirm task is now delayed by several minutes.
 698          $task = manager::get_scheduled_task($taskname);
 699          $this->assertEquals(240, $task->get_fail_delay());
 700          $this->assertGreaterThan($before + 230, $task->get_next_run_time());
 701  
 702          // Clear the fail delay and re-get the task.
 703          manager::clear_fail_delay($task);
 704          $task = manager::get_scheduled_task($taskname);
 705  
 706          // There should be no delay and it should run within the next minute.
 707          $this->assertEquals(0, $task->get_fail_delay());
 708          $this->assertLessThan($before + 70, $task->get_next_run_time());
 709      }
 710  
 711      /**
 712       * Data provider for test_scheduled_task_override_values.
 713       */
 714      public static function provider_schedule_overrides(): array {
 715          return array(
 716              array(
 717                  'scheduled_tasks' => array(
 718                      '\core\task\scheduled_test_task' => array(
 719                          'schedule' => '10 13 1 2 4',
 720                          'disabled' => 0,
 721                      ),
 722                      '\core\task\scheduled_test2_task' => array(
 723                          'schedule' => '* * * * *',
 724                          'disabled' => 1,
 725                      ),
 726                  ),
 727                  'task_full_classnames' => array(
 728                      '\core\task\scheduled_test_task',
 729                      '\core\task\scheduled_test2_task',
 730                  ),
 731                  'expected' => array(
 732                      '\core\task\scheduled_test_task' => array(
 733                          'min'   => '10',
 734                          'hour'  => '13',
 735                          'day'   => '1',
 736                          'month' => '2',
 737                          'week'  => '4',
 738                          'disabled' => 0,
 739                      ),
 740                      '\core\task\scheduled_test2_task' => array(
 741                          'min'   => '*',
 742                          'hour'  => '*',
 743                          'day'   => '*',
 744                          'month' => '*',
 745                          'week'  => '*',
 746                          'disabled' => 1,
 747                      ),
 748                  )
 749              ),
 750              array(
 751                  'scheduled_tasks' => array(
 752                      '\core\task\*' => array(
 753                          'schedule' => '1 2 3 4 5',
 754                          'disabled' => 0,
 755                      )
 756                  ),
 757                  'task_full_classnames' => array(
 758                      '\core\task\scheduled_test_task',
 759                      '\core\task\scheduled_test2_task',
 760                  ),
 761                  'expected' => array(
 762                      '\core\task\scheduled_test_task' => array(
 763                          'min'   => '1',
 764                          'hour'  => '2',
 765                          'day'   => '3',
 766                          'month' => '4',
 767                          'week'  => '5',
 768                          'disabled' => 0,
 769                      ),
 770                      '\core\task\scheduled_test2_task' => array(
 771                          'min'   => '1',
 772                          'hour'  => '2',
 773                          'day'   => '3',
 774                          'month' => '4',
 775                          'week'  => '5',
 776                          'disabled' => 0,
 777                      ),
 778                  )
 779              )
 780          );
 781      }
 782  
 783  
 784      /**
 785       * Test to ensure scheduled tasks are updated by values set in config.
 786       *
 787       * @param array $overrides
 788       * @param array $tasks
 789       * @param array $expected
 790       * @dataProvider provider_schedule_overrides
 791       */
 792      public function test_scheduled_task_override_values(array $overrides, array $tasks, array $expected): void {
 793          global $CFG, $DB;
 794  
 795          $this->resetAfterTest();
 796  
 797          // Add overrides to the config.
 798          $CFG->scheduled_tasks = $overrides;
 799  
 800          // Set up test scheduled task record.
 801          $record = new \stdClass();
 802          $record->component = 'test_scheduled_task';
 803  
 804          foreach ($tasks as $task) {
 805              $record->classname = $task;
 806              $DB->insert_record('task_scheduled', $record);
 807  
 808              $scheduledtask = manager::get_scheduled_task($task);
 809              $expectedresults = $expected[$task];
 810  
 811              // Check that the task is actually overridden.
 812              $this->assertTrue($scheduledtask->is_overridden(), 'Is overridden');
 813  
 814              // Check minute is correct.
 815              $this->assertEquals($expectedresults['min'], $scheduledtask->get_minute(), 'Minute check');
 816  
 817              // Check day is correct.
 818              $this->assertEquals($expectedresults['day'], $scheduledtask->get_day(), 'Day check');
 819  
 820              // Check hour is correct.
 821              $this->assertEquals($expectedresults['hour'], $scheduledtask->get_hour(), 'Hour check');
 822  
 823              // Check week is correct.
 824              $this->assertEquals($expectedresults['week'], $scheduledtask->get_day_of_week(), 'Day of week check');
 825  
 826              // Check week is correct.
 827              $this->assertEquals($expectedresults['month'], $scheduledtask->get_month(), 'Month check');
 828  
 829              // Check to see if the task is disabled.
 830              $this->assertEquals($expectedresults['disabled'], $scheduledtask->get_disabled(), 'Disabled check');
 831          }
 832      }
 833  
 834      /**
 835       * Check that an overridden task is sent to be processed.
 836       */
 837      public function test_scheduled_task_overridden_task_can_run(): void {
 838          global $CFG, $DB;
 839  
 840          $this->resetAfterTest();
 841  
 842          // Delete all existing scheduled tasks.
 843          $DB->delete_records('task_scheduled');
 844  
 845          // Add overrides to the config.
 846          $CFG->scheduled_tasks = [
 847              '\core\task\scheduled_test_task' => [
 848                  'disabled' => 1
 849              ],
 850              '\core\task\scheduled_test2_task' => [
 851                  'disabled' => 0
 852              ],
 853          ];
 854  
 855          // A task that runs once per hour.
 856          $record = new \stdClass();
 857          $record->component = 'test_scheduled_task';
 858          $record->classname = '\core\task\scheduled_test_task';
 859          $record->disabled = 0;
 860          $DB->insert_record('task_scheduled', $record);
 861  
 862          // And disabled test.
 863          $record->classname = '\core\task\scheduled_test2_task';
 864          $record->disabled = 1;
 865          $DB->insert_record('task_scheduled', $record);
 866  
 867          $now = time();
 868  
 869          $scheduledtask = manager::get_next_scheduled_task($now);
 870          $this->assertInstanceOf('\core\task\scheduled_test2_task', $scheduledtask);
 871          $scheduledtask->execute();
 872          manager::scheduled_task_complete($scheduledtask);
 873      }
 874  
 875      /**
 876       * Assert that the specified tasks are equal.
 877       *
 878       * @param   \core\task\task_base $task
 879       * @param   \core\task\task_base $comparisontask
 880       */
 881      public function assertTaskEquals(task_base $task, task_base $comparisontask): void {
 882          // Convert both to an object.
 883          $task = manager::record_from_scheduled_task($task);
 884          $comparisontask = manager::record_from_scheduled_task($comparisontask);
 885  
 886          // Reset the nextruntime field as it is intentionally dynamic.
 887          $task->nextruntime = null;
 888          $comparisontask->nextruntime = null;
 889  
 890          $args = array_merge(
 891              [
 892                  $task,
 893                  $comparisontask,
 894              ],
 895              array_slice(func_get_args(), 2)
 896          );
 897  
 898          call_user_func_array([$this, 'assertEquals'], $args);
 899      }
 900  
 901      /**
 902       * Assert that the specified tasks are not equal.
 903       *
 904       * @param   \core\task\task_base $task
 905       * @param   \core\task\task_base $comparisontask
 906       */
 907      public function assertTaskNotEquals(task_base $task, task_base $comparisontask): void {
 908          // Convert both to an object.
 909          $task = manager::record_from_scheduled_task($task);
 910          $comparisontask = manager::record_from_scheduled_task($comparisontask);
 911  
 912          // Reset the nextruntime field as it is intentionally dynamic.
 913          $task->nextruntime = null;
 914          $comparisontask->nextruntime = null;
 915  
 916          $args = array_merge(
 917              [
 918                  $task,
 919                  $comparisontask,
 920              ],
 921              array_slice(func_get_args(), 2)
 922          );
 923  
 924          call_user_func_array([$this, 'assertNotEquals'], $args);
 925      }
 926  
 927      /**
 928       * Assert that the lastruntime column holds an original value after a scheduled task is reset.
 929       */
 930      public function test_reset_scheduled_tasks_for_component_keeps_original_lastruntime(): void {
 931          global $DB;
 932          $this->resetAfterTest(true);
 933  
 934          // Set lastruntime for the scheduled task.
 935          $DB->set_field('task_scheduled', 'lastruntime', 123456789, ['classname' => '\core\task\session_cleanup_task']);
 936  
 937          // Reset the task.
 938          manager::reset_scheduled_tasks_for_component('moodle');
 939  
 940          // Fetch the task again.
 941          $taskafterreset = manager::get_scheduled_task(session_cleanup_task::class);
 942  
 943          // Confirm, that lastruntime is still in place.
 944          $this->assertEquals(123456789, $taskafterreset->get_last_run_time());
 945      }
 946  
 947      /**
 948       * Data provider for {@see test_is_component_enabled}
 949       *
 950       * @return array[]
 951       */
 952      public function is_component_enabled_provider(): array {
 953          return [
 954              'Enabled component' => ['auth_cas', true],
 955              'Disabled component' => ['auth_ldap', false],
 956              'Invalid component' => ['auth_invalid', false],
 957          ];
 958      }
 959  
 960      /**
 961       * Tests whether tasks belonging to components consider the component to be enabled
 962       *
 963       * @param string $component
 964       * @param bool $expected
 965       *
 966       * @dataProvider is_component_enabled_provider
 967       */
 968      public function test_is_component_enabled(string $component, bool $expected): void {
 969          $this->resetAfterTest();
 970  
 971          // Set cas as the only enabled auth component.
 972          set_config('auth', 'cas');
 973  
 974          $task = new scheduled_test_task();
 975          $task->set_component($component);
 976  
 977          $this->assertEquals($expected, $task->is_component_enabled());
 978      }
 979  
 980      /**
 981       * Test whether tasks belonging to core components considers the component to be enabled
 982       */
 983      public function test_is_component_enabled_core(): void {
 984          $task = new scheduled_test_task();
 985          $this->assertTrue($task->is_component_enabled());
 986      }
 987  }