See Release Notes
Long Term Support Release
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body