Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 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   */
  30  class scheduled_task_test extends \advanced_testcase {
  31  
  32      /**
  33       * Test the cron scheduling method
  34       */
  35      public function test_eval_cron_field() {
  36          $testclass = new scheduled_test_task();
  37  
  38          $this->assertEquals(20, count($testclass->eval_cron_field('*/3', 0, 59)));
  39          $this->assertEquals(31, count($testclass->eval_cron_field('1,*/2', 0, 59)));
  40          $this->assertEquals(15, count($testclass->eval_cron_field('1-10,5-15', 0, 59)));
  41          $this->assertEquals(13, count($testclass->eval_cron_field('1-10,5-15/2', 0, 59)));
  42          $this->assertEquals(3, count($testclass->eval_cron_field('1,2,3,1,2,3', 0, 59)));
  43          $this->assertEquals(1, count($testclass->eval_cron_field('-1,10,80', 0, 59)));
  44      }
  45  
  46      public function test_get_next_scheduled_time() {
  47          global $CFG;
  48          $this->resetAfterTest();
  49  
  50          $this->setTimezone('Europe/London');
  51  
  52          // Let's specify the hour we are going to use initially for the test.
  53          // (note that we pick 01:00 that is tricky for Europe/London, because
  54          // it's exactly the Daylight Saving Time Begins hour.
  55          $testhour = 1;
  56  
  57          // Test job run at 1 am.
  58          $testclass = new scheduled_test_task();
  59  
  60          // All fields default to '*'.
  61          $testclass->set_hour($testhour);
  62          $testclass->set_minute('0');
  63          // Next valid time should be 1am of the next day.
  64          $nexttime = $testclass->get_next_scheduled_time();
  65  
  66          $oneamdate = new \DateTime('now', new \DateTimeZone('Europe/London'));
  67          $oneamdate->setTime($testhour, 0, 0);
  68  
  69          // Once a year (currently last Sunday of March), when changing to Daylight Saving Time,
  70          // Europe/London 01:00 simply doesn't exists because, exactly at 01:00 the clock
  71          // is advanced by one hour and becomes 02:00. When that happens, the DateInterval
  72          // calculations cannot be to advance by 1 day, but by one less hour. That is exactly when
  73          // the next scheduled run will happen (next day 01:00).
  74          $isdaylightsaving = false;
  75          if ($testhour < (int)$oneamdate->format('H')) {
  76              $isdaylightsaving = true;
  77          }
  78  
  79          // Make it 1 am tomorrow if the time is after 1am.
  80          if ($oneamdate->getTimestamp() < time()) {
  81              $oneamdate->add(new \DateInterval('P1D'));
  82              if ($isdaylightsaving) {
  83                  // If today is Europe/London Daylight Saving Time Begins, expectation is 1 less hour.
  84                  $oneamdate->sub(new \DateInterval('PT1H'));
  85              }
  86          }
  87          $oneam = $oneamdate->getTimestamp();
  88  
  89          $this->assertEquals($oneam, $nexttime, 'Next scheduled time is 1am.');
  90  
  91          // Disabled flag does not affect next time.
  92          $testclass->set_disabled(true);
  93          $nexttime = $testclass->get_next_scheduled_time();
  94          $this->assertEquals($oneam, $nexttime, 'Next scheduled time is 1am.');
  95  
  96          // Now test for job run every 10 minutes.
  97          $testclass = new scheduled_test_task();
  98  
  99          // All fields default to '*'.
 100          $testclass->set_minute('*/10');
 101          // Next valid time should be next 10 minute boundary.
 102          $nexttime = $testclass->get_next_scheduled_time();
 103  
 104          $minutes = ((intval(date('i') / 10))+1) * 10;
 105          $nexttenminutes = mktime(date('H'), $minutes, 0);
 106  
 107          $this->assertEquals($nexttenminutes, $nexttime, 'Next scheduled time is in 10 minutes.');
 108  
 109          // Disabled flag does not affect next time.
 110          $testclass->set_disabled(true);
 111          $nexttime = $testclass->get_next_scheduled_time();
 112          $this->assertEquals($nexttenminutes, $nexttime, 'Next scheduled time is in 10 minutes.');
 113  
 114          // Test hourly job executed on Sundays only.
 115          $testclass = new scheduled_test_task();
 116          $testclass->set_minute('0');
 117          $testclass->set_day_of_week('7');
 118  
 119          $nexttime = $testclass->get_next_scheduled_time();
 120  
 121          $this->assertEquals(7, date('N', $nexttime));
 122          $this->assertEquals(0, date('i', $nexttime));
 123  
 124          // Test monthly job.
 125          $testclass = new scheduled_test_task();
 126          $testclass->set_minute('32');
 127          $testclass->set_hour('0');
 128          $testclass->set_day('1');
 129  
 130          $nexttime = $testclass->get_next_scheduled_time();
 131  
 132          $this->assertEquals(32, date('i', $nexttime));
 133          $this->assertEquals(0, date('G', $nexttime));
 134          $this->assertEquals(1, date('j', $nexttime));
 135      }
 136  
 137      public function test_timezones() {
 138          global $CFG, $USER;
 139  
 140          // The timezones used in this test are chosen because they do not use DST - that would break the test.
 141          $this->resetAfterTest();
 142  
 143          $this->setTimezone('Asia/Kabul');
 144  
 145          $testclass = new scheduled_test_task();
 146  
 147          // Scheduled tasks should always use servertime - so this is 03:30 GMT.
 148          $testclass->set_hour('1');
 149          $testclass->set_minute('0');
 150  
 151          // Next valid time should be 1am of the next day.
 152          $nexttime = $testclass->get_next_scheduled_time();
 153  
 154          // GMT+05:45.
 155          $USER->timezone = 'Asia/Kathmandu';
 156          $userdate = userdate($nexttime);
 157  
 158          // Should be displayed in user timezone.
 159          // I used http://www.timeanddate.com/worldclock/fixedtime.html?msg=Moodle+Test&iso=20160502T01&p1=113
 160          // setting my location to Kathmandu to verify this time.
 161          $this->assertStringContainsString('2:15 AM', \core_text::strtoupper($userdate));
 162      }
 163  
 164      public function test_reset_scheduled_tasks_for_component_customised(): void {
 165          $this->resetAfterTest(true);
 166  
 167          $tasks = manager::load_scheduled_tasks_for_component('moodle');
 168  
 169          // Customise a task.
 170          $task = reset($tasks);
 171          $task->set_minute('1');
 172          $task->set_hour('2');
 173          $task->set_day('3');
 174          $task->set_month('4');
 175          $task->set_day_of_week('5');
 176          $task->set_customised('1');
 177          manager::configure_scheduled_task($task);
 178  
 179          // Now call reset.
 180          manager::reset_scheduled_tasks_for_component('moodle');
 181  
 182          // Fetch the task again.
 183          $taskafterreset = manager::get_scheduled_task(get_class($task));
 184  
 185          // The task should still be the same as the customised.
 186          $this->assertTaskEquals($task, $taskafterreset);
 187      }
 188  
 189      public function test_reset_scheduled_tasks_for_component_deleted(): void {
 190          global $DB;
 191          $this->resetAfterTest(true);
 192  
 193          // Delete a task to simulate the fact that its new.
 194          $tasklist = manager::load_scheduled_tasks_for_component('moodle');
 195  
 196          // Note: This test must use a task which does not use any random values.
 197          $task = manager::get_scheduled_task(session_cleanup_task::class);
 198  
 199          $DB->delete_records('task_scheduled', array('classname' => '\\' . trim(get_class($task), '\\')));
 200          $this->assertFalse(manager::get_scheduled_task(session_cleanup_task::class));
 201  
 202          // Now call reset on all the tasks.
 203          manager::reset_scheduled_tasks_for_component('moodle');
 204  
 205          // Assert that the second task was added back.
 206          $taskafterreset = manager::get_scheduled_task(session_cleanup_task::class);
 207          $this->assertNotFalse($taskafterreset);
 208  
 209          $this->assertTaskEquals($task, $taskafterreset);
 210          $this->assertCount(count($tasklist), manager::load_scheduled_tasks_for_component('moodle'));
 211      }
 212  
 213      public function test_reset_scheduled_tasks_for_component_changed_in_source(): void {
 214          $this->resetAfterTest(true);
 215  
 216          // Delete a task to simulate the fact that its new.
 217          // Note: This test must use a task which does not use any random values.
 218          $task = manager::get_scheduled_task(session_cleanup_task::class);
 219  
 220          // Get a copy of the task before maing changes for later comparison.
 221          $taskbeforechange = manager::get_scheduled_task(session_cleanup_task::class);
 222  
 223          // Edit a task to simulate a change in its definition (as if it was not customised).
 224          $task->set_minute('1');
 225          $task->set_hour('2');
 226          $task->set_day('3');
 227          $task->set_month('4');
 228          $task->set_day_of_week('5');
 229          manager::configure_scheduled_task($task);
 230  
 231          // Fetch the task out for comparison.
 232          $taskafterchange = manager::get_scheduled_task(session_cleanup_task::class);
 233  
 234          // The task should now be different to the original.
 235          $this->assertTaskNotEquals($taskbeforechange, $taskafterchange);
 236  
 237          // Now call reset.
 238          manager::reset_scheduled_tasks_for_component('moodle');
 239  
 240          // Fetch the task again.
 241          $taskafterreset = manager::get_scheduled_task(session_cleanup_task::class);
 242  
 243          // The task should now be the same as the original.
 244          $this->assertTaskEquals($taskbeforechange, $taskafterreset);
 245      }
 246  
 247      /**
 248       * Tests that the reset function deletes old tasks.
 249       */
 250      public function test_reset_scheduled_tasks_for_component_delete() {
 251          global $DB;
 252          $this->resetAfterTest(true);
 253  
 254          $count = $DB->count_records('task_scheduled', array('component' => 'moodle'));
 255          $allcount = $DB->count_records('task_scheduled');
 256  
 257          $task = new scheduled_test_task();
 258          $task->set_component('moodle');
 259          $record = manager::record_from_scheduled_task($task);
 260          $DB->insert_record('task_scheduled', $record);
 261          $this->assertTrue($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test_task',
 262              'component' => 'moodle')));
 263  
 264          $task = new scheduled_test2_task();
 265          $task->set_component('moodle');
 266          $record = manager::record_from_scheduled_task($task);
 267          $DB->insert_record('task_scheduled', $record);
 268          $this->assertTrue($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test2_task',
 269              'component' => 'moodle')));
 270  
 271          $aftercount = $DB->count_records('task_scheduled', array('component' => 'moodle'));
 272          $afterallcount = $DB->count_records('task_scheduled');
 273  
 274          $this->assertEquals($count + 2, $aftercount);
 275          $this->assertEquals($allcount + 2, $afterallcount);
 276  
 277          // Now check that the right things were deleted.
 278          manager::reset_scheduled_tasks_for_component('moodle');
 279  
 280          $this->assertEquals($count, $DB->count_records('task_scheduled', array('component' => 'moodle')));
 281          $this->assertEquals($allcount, $DB->count_records('task_scheduled'));
 282          $this->assertFalse($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test2_task',
 283              'component' => 'moodle')));
 284          $this->assertFalse($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test_task',
 285              'component' => 'moodle')));
 286      }
 287  
 288      public function test_get_next_scheduled_task() {
 289          global $DB;
 290  
 291          $this->resetAfterTest(true);
 292          // Delete all existing scheduled tasks.
 293          $DB->delete_records('task_scheduled');
 294          // Add a scheduled task.
 295  
 296          // A task that runs once per hour.
 297          $record = new \stdClass();
 298          $record->blocking = true;
 299          $record->minute = '0';
 300          $record->hour = '0';
 301          $record->dayofweek = '*';
 302          $record->day = '*';
 303          $record->month = '*';
 304          $record->component = 'test_scheduled_task';
 305          $record->classname = '\core\task\scheduled_test_task';
 306  
 307          $DB->insert_record('task_scheduled', $record);
 308          // And another one to test failures.
 309          $record->classname = '\core\task\scheduled_test2_task';
 310          $DB->insert_record('task_scheduled', $record);
 311          // And disabled test.
 312          $record->classname = '\core\task\scheduled_test3_task';
 313          $record->disabled = 1;
 314          $DB->insert_record('task_scheduled', $record);
 315  
 316          $now = time();
 317  
 318          // Should get handed the first task.
 319          $task = manager::get_next_scheduled_task($now);
 320          $this->assertInstanceOf('\core\task\scheduled_test_task', $task);
 321          $task->execute();
 322  
 323          manager::scheduled_task_complete($task);
 324          // Should get handed the second task.
 325          $task = manager::get_next_scheduled_task($now);
 326          $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
 327          $task->execute();
 328  
 329          manager::scheduled_task_failed($task);
 330          // Should not get any task.
 331          $task = manager::get_next_scheduled_task($now);
 332          $this->assertNull($task);
 333  
 334          // Should get the second task (retry after delay).
 335          $task = manager::get_next_scheduled_task($now + 120);
 336          $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
 337          $task->execute();
 338  
 339          manager::scheduled_task_complete($task);
 340  
 341          // Should not get any task.
 342          $task = manager::get_next_scheduled_task($now);
 343          $this->assertNull($task);
 344  
 345          // Check ordering.
 346          $DB->delete_records('task_scheduled');
 347          $record->lastruntime = 2;
 348          $record->disabled = 0;
 349          $record->classname = '\core\task\scheduled_test_task';
 350          $DB->insert_record('task_scheduled', $record);
 351  
 352          $record->lastruntime = 1;
 353          $record->classname = '\core\task\scheduled_test2_task';
 354          $DB->insert_record('task_scheduled', $record);
 355  
 356          // Should get handed the second task.
 357          $task = manager::get_next_scheduled_task($now);
 358          $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
 359          $task->execute();
 360          manager::scheduled_task_complete($task);
 361  
 362          // Should get handed the first task.
 363          $task = manager::get_next_scheduled_task($now);
 364          $this->assertInstanceOf('\core\task\scheduled_test_task', $task);
 365          $task->execute();
 366          manager::scheduled_task_complete($task);
 367  
 368          // Should not get any task.
 369          $task = manager::get_next_scheduled_task($now);
 370          $this->assertNull($task);
 371      }
 372  
 373      public function test_get_broken_scheduled_task() {
 374          global $DB;
 375  
 376          $this->resetAfterTest(true);
 377          // Delete all existing scheduled tasks.
 378          $DB->delete_records('task_scheduled');
 379          // Add a scheduled task.
 380  
 381          // A broken task that runs all the time.
 382          $record = new \stdClass();
 383          $record->blocking = true;
 384          $record->minute = '*';
 385          $record->hour = '*';
 386          $record->dayofweek = '*';
 387          $record->day = '*';
 388          $record->month = '*';
 389          $record->component = 'test_scheduled_task';
 390          $record->classname = '\core\task\scheduled_test_task_broken';
 391  
 392          $DB->insert_record('task_scheduled', $record);
 393  
 394          $now = time();
 395          // Should not get any task.
 396          $task = manager::get_next_scheduled_task($now);
 397          $this->assertDebuggingCalled();
 398          $this->assertNull($task);
 399      }
 400  
 401      /**
 402       * Tests the use of 'R' syntax in time fields of tasks to get
 403       * tasks be configured with a non-uniform time.
 404       */
 405      public function test_random_time_specification() {
 406  
 407          // Testing non-deterministic things in a unit test is not really
 408          // wise, so we just test the values have changed within allowed bounds.
 409          $testclass = new scheduled_test_task();
 410  
 411          // The test task defaults to '*'.
 412          $this->assertIsString($testclass->get_minute());
 413          $this->assertIsString($testclass->get_hour());
 414  
 415          // Set a random value.
 416          $testclass->set_minute('R');
 417          $testclass->set_hour('R');
 418          $testclass->set_day_of_week('R');
 419  
 420          // Verify the minute has changed within allowed bounds.
 421          $minute = $testclass->get_minute();
 422          $this->assertIsInt($minute);
 423          $this->assertGreaterThanOrEqual(0, $minute);
 424          $this->assertLessThanOrEqual(59, $minute);
 425  
 426          // Verify the hour has changed within allowed bounds.
 427          $hour = $testclass->get_hour();
 428          $this->assertIsInt($hour);
 429          $this->assertGreaterThanOrEqual(0, $hour);
 430          $this->assertLessThanOrEqual(23, $hour);
 431  
 432          // Verify the dayofweek has changed within allowed bounds.
 433          $dayofweek = $testclass->get_day_of_week();
 434          $this->assertIsInt($dayofweek);
 435          $this->assertGreaterThanOrEqual(0, $dayofweek);
 436          $this->assertLessThanOrEqual(6, $dayofweek);
 437      }
 438  
 439      /**
 440       * Test that the file_temp_cleanup_task removes directories and
 441       * files as expected.
 442       */
 443      public function test_file_temp_cleanup_task() {
 444          global $CFG;
 445          $backuptempdir = make_backup_temp_directory('');
 446  
 447          // Create directories.
 448          $dir = $backuptempdir . DIRECTORY_SEPARATOR . 'backup01' . DIRECTORY_SEPARATOR . 'courses';
 449          mkdir($dir, 0777, true);
 450  
 451          // Create files to be checked and then deleted.
 452          $file01 = $dir . DIRECTORY_SEPARATOR . 'sections.xml';
 453          file_put_contents($file01, 'test data 001');
 454          $file02 = $dir . DIRECTORY_SEPARATOR . 'modules.xml';
 455          file_put_contents($file02, 'test data 002');
 456          // Change the time modified for the first file, to a time that will be deleted by the task (greater than seven days).
 457          touch($file01, time() - (8 * 24 * 3600));
 458  
 459          $task = manager::get_scheduled_task('\\core\\task\\file_temp_cleanup_task');
 460          $this->assertInstanceOf('\core\task\file_temp_cleanup_task', $task);
 461          $task->execute();
 462  
 463          // Scan the directory. Only modules.xml should be left.
 464          $filesarray = scandir($dir);
 465          $this->assertEquals('modules.xml', $filesarray[2]);
 466          $this->assertEquals(3, count($filesarray));
 467  
 468          // Change the time modified on modules.xml.
 469          touch($file02, time() - (8 * 24 * 3600));
 470          // Change the time modified on the courses directory.
 471          touch($backuptempdir . DIRECTORY_SEPARATOR . 'backup01' . DIRECTORY_SEPARATOR .
 472                  'courses', time() - (8 * 24 * 3600));
 473          // Run the scheduled task to remove the file and directory.
 474          $task->execute();
 475          $filesarray = scandir($backuptempdir . DIRECTORY_SEPARATOR . 'backup01');
 476          // There should only be two items in the array, '.' and '..'.
 477          $this->assertEquals(2, count($filesarray));
 478  
 479          // Change the time modified on all of the files and directories.
 480          $dir = new \RecursiveDirectoryIterator($CFG->tempdir);
 481          // Show all child nodes prior to their parent.
 482          $iter = new \RecursiveIteratorIterator($dir, \RecursiveIteratorIterator::CHILD_FIRST);
 483  
 484          for ($iter->rewind(); $iter->valid(); $iter->next()) {
 485              if ($iter->isDir() && !$iter->isDot()) {
 486                  $node = $iter->getRealPath();
 487                  touch($node, time() - (8 * 24 * 3600));
 488              }
 489          }
 490  
 491          // Run the scheduled task again to remove all of the files and directories.
 492          $task->execute();
 493          $filesarray = scandir($CFG->tempdir);
 494          // All of the files and directories should be deleted.
 495          // There should only be three items in the array, '.', '..' and '.htaccess'.
 496          $this->assertEquals([ '.', '..', '.htaccess' ], $filesarray);
 497      }
 498  
 499      /**
 500       * Test that the function to clear the fail delay from a task works correctly.
 501       */
 502      public function test_clear_fail_delay() {
 503  
 504          $this->resetAfterTest();
 505  
 506          // Get an example task to use for testing. Task is set to run every minute by default.
 507          $taskname = '\core\task\send_new_user_passwords_task';
 508  
 509          // Pretend task started running and then failed 3 times.
 510          $before = time();
 511          $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
 512          for ($i = 0; $i < 3; $i ++) {
 513              $task = manager::get_scheduled_task($taskname);
 514              $lock = $cronlockfactory->get_lock('\\' . get_class($task), 10);
 515              $task->set_lock($lock);
 516              manager::scheduled_task_failed($task);
 517          }
 518  
 519          // Confirm task is now delayed by several minutes.
 520          $task = manager::get_scheduled_task($taskname);
 521          $this->assertEquals(240, $task->get_fail_delay());
 522          $this->assertGreaterThan($before + 230, $task->get_next_run_time());
 523  
 524          // Clear the fail delay and re-get the task.
 525          manager::clear_fail_delay($task);
 526          $task = manager::get_scheduled_task($taskname);
 527  
 528          // There should be no delay and it should run within the next minute.
 529          $this->assertEquals(0, $task->get_fail_delay());
 530          $this->assertLessThan($before + 70, $task->get_next_run_time());
 531      }
 532  
 533      /**
 534       * Data provider for test_tool_health_category_find_missing_parents.
 535       */
 536      public static function provider_schedule_overrides(): array {
 537          return array(
 538              array(
 539                  'scheduled_tasks' => array(
 540                      '\core\task\scheduled_test_task' => array(
 541                          'schedule' => '10 13 1 2 4',
 542                          'disabled' => 0,
 543                      ),
 544                      '\core\task\scheduled_test2_task' => array(
 545                          'schedule' => '* * * * *',
 546                          'disabled' => 1,
 547                      ),
 548                  ),
 549                  'task_full_classnames' => array(
 550                      '\core\task\scheduled_test_task',
 551                      '\core\task\scheduled_test2_task',
 552                  ),
 553                  'expected' => array(
 554                      '\core\task\scheduled_test_task' => array(
 555                          'min'   => '10',
 556                          'hour'  => '13',
 557                          'day'   => '1',
 558                          'month' => '2',
 559                          'week'  => '4',
 560                          'disabled' => 0,
 561                      ),
 562                      '\core\task\scheduled_test2_task' => array(
 563                          'min'   => '*',
 564                          'hour'  => '*',
 565                          'day'   => '*',
 566                          'month' => '*',
 567                          'week'  => '*',
 568                          'disabled' => 1,
 569                      ),
 570                  )
 571              ),
 572              array(
 573                  'scheduled_tasks' => array(
 574                      '\core\task\*' => array(
 575                          'schedule' => '1 2 3 4 5',
 576                          'disabled' => 0,
 577                      )
 578                  ),
 579                  'task_full_classnames' => array(
 580                      '\core\task\scheduled_test_task',
 581                      '\core\task\scheduled_test2_task',
 582                  ),
 583                  'expected' => array(
 584                      '\core\task\scheduled_test_task' => array(
 585                          'min'   => '1',
 586                          'hour'  => '2',
 587                          'day'   => '3',
 588                          'month' => '4',
 589                          'week'  => '5',
 590                          'disabled' => 0,
 591                      ),
 592                      '\core\task\scheduled_test2_task' => array(
 593                          'min'   => '1',
 594                          'hour'  => '2',
 595                          'day'   => '3',
 596                          'month' => '4',
 597                          'week'  => '5',
 598                          'disabled' => 0,
 599                      ),
 600                  )
 601              )
 602          );
 603      }
 604  
 605  
 606      /**
 607       * Test to ensure scheduled tasks are updated by values set in config.
 608       *
 609       * @param array $overrides
 610       * @param array $tasks
 611       * @param array $expected
 612       * @dataProvider provider_schedule_overrides
 613       */
 614      public function test_scheduled_task_override_values(array $overrides, array $tasks, array $expected): void {
 615          global $CFG, $DB;
 616  
 617          $this->resetAfterTest();
 618  
 619          // Add overrides to the config.
 620          $CFG->scheduled_tasks = $overrides;
 621  
 622          // Set up test scheduled task record.
 623          $record = new \stdClass();
 624          $record->component = 'test_scheduled_task';
 625  
 626          foreach ($tasks as $task) {
 627              $record->classname = $task;
 628              $DB->insert_record('task_scheduled', $record);
 629  
 630              $scheduledtask = manager::get_scheduled_task($task);
 631              $expectedresults = $expected[$task];
 632  
 633              // Check that the task is actually overridden.
 634              $this->assertTrue($scheduledtask->is_overridden(), 'Is overridden');
 635  
 636              // Check minute is correct.
 637              $this->assertEquals($expectedresults['min'], $scheduledtask->get_minute(), 'Minute check');
 638  
 639              // Check day is correct.
 640              $this->assertEquals($expectedresults['day'], $scheduledtask->get_day(), 'Day check');
 641  
 642              // Check hour is correct.
 643              $this->assertEquals($expectedresults['hour'], $scheduledtask->get_hour(), 'Hour check');
 644  
 645              // Check week is correct.
 646              $this->assertEquals($expectedresults['week'], $scheduledtask->get_day_of_week(), 'Day of week check');
 647  
 648              // Check week is correct.
 649              $this->assertEquals($expectedresults['month'], $scheduledtask->get_month(), 'Month check');
 650  
 651              // Check to see if the task is disabled.
 652              $this->assertEquals($expectedresults['disabled'], $scheduledtask->get_disabled(), 'Disabled check');
 653          }
 654      }
 655  
 656      /**
 657       * Check that an overridden task is sent to be processed.
 658       */
 659      public function test_scheduled_task_overridden_task_can_run(): void {
 660          global $CFG, $DB;
 661  
 662          $this->resetAfterTest();
 663  
 664          // Delete all existing scheduled tasks.
 665          $DB->delete_records('task_scheduled');
 666  
 667          // Add overrides to the config.
 668          $CFG->scheduled_tasks = [
 669              '\core\task\scheduled_test_task' => [
 670                  'disabled' => 1
 671              ],
 672              '\core\task\scheduled_test2_task' => [
 673                  'disabled' => 0
 674              ],
 675          ];
 676  
 677          // A task that runs once per hour.
 678          $record = new \stdClass();
 679          $record->component = 'test_scheduled_task';
 680          $record->classname = '\core\task\scheduled_test_task';
 681          $record->disabled = 0;
 682          $DB->insert_record('task_scheduled', $record);
 683  
 684          // And disabled test.
 685          $record->classname = '\core\task\scheduled_test2_task';
 686          $record->disabled = 1;
 687          $DB->insert_record('task_scheduled', $record);
 688  
 689          $now = time();
 690  
 691          $scheduledtask = manager::get_next_scheduled_task($now);
 692          $this->assertInstanceOf('\core\task\scheduled_test2_task', $scheduledtask);
 693          $scheduledtask->execute();
 694          manager::scheduled_task_complete($scheduledtask);
 695      }
 696  
 697      /**
 698       * Assert that the specified tasks are equal.
 699       *
 700       * @param   \core\task\task_base $task
 701       * @param   \core\task\task_base $comparisontask
 702       */
 703      public function assertTaskEquals(task_base $task, task_base $comparisontask): void {
 704          // Convert both to an object.
 705          $task = manager::record_from_scheduled_task($task);
 706          $comparisontask = manager::record_from_scheduled_task($comparisontask);
 707  
 708          // Reset the nextruntime field as it is intentionally dynamic.
 709          $task->nextruntime = null;
 710          $comparisontask->nextruntime = null;
 711  
 712          $args = array_merge(
 713              [
 714                  $task,
 715                  $comparisontask,
 716              ],
 717              array_slice(func_get_args(), 2)
 718          );
 719  
 720          call_user_func_array([$this, 'assertEquals'], $args);
 721      }
 722  
 723      /**
 724       * Assert that the specified tasks are not equal.
 725       *
 726       * @param   \core\task\task_base $task
 727       * @param   \core\task\task_base $comparisontask
 728       */
 729      public function assertTaskNotEquals(task_base $task, task_base $comparisontask): void {
 730          // Convert both to an object.
 731          $task = manager::record_from_scheduled_task($task);
 732          $comparisontask = manager::record_from_scheduled_task($comparisontask);
 733  
 734          // Reset the nextruntime field as it is intentionally dynamic.
 735          $task->nextruntime = null;
 736          $comparisontask->nextruntime = null;
 737  
 738          $args = array_merge(
 739              [
 740                  $task,
 741                  $comparisontask,
 742              ],
 743              array_slice(func_get_args(), 2)
 744          );
 745  
 746          call_user_func_array([$this, 'assertNotEquals'], $args);
 747      }
 748  
 749      /**
 750       * Assert that the lastruntime column holds an original value after a scheduled task is reset.
 751       */
 752      public function test_reset_scheduled_tasks_for_component_keeps_original_lastruntime(): void {
 753          global $DB;
 754          $this->resetAfterTest(true);
 755  
 756          // Set lastruntime for the scheduled task.
 757          $DB->set_field('task_scheduled', 'lastruntime', 123456789, ['classname' => '\core\task\session_cleanup_task']);
 758  
 759          // Reset the task.
 760          manager::reset_scheduled_tasks_for_component('moodle');
 761  
 762          // Fetch the task again.
 763          $taskafterreset = manager::get_scheduled_task(session_cleanup_task::class);
 764  
 765          // Confirm, that lastruntime is still in place.
 766          $this->assertEquals(123456789, $taskafterreset->get_last_run_time());
 767      }
 768  }