Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

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