. /** * This file contains the unittests for scheduled tasks. * * @package core * @category phpunit * @copyright 2013 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/fixtures/task_fixtures.php'); /** * Test class for scheduled task. * * @package core * @category task * @copyright 2013 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_scheduled_task_testcase extends advanced_testcase { /** * Test the cron scheduling method */ public function test_eval_cron_field() { $testclass = new \core\task\scheduled_test_task(); $this->assertEquals(20, count($testclass->eval_cron_field('*/3', 0, 59))); $this->assertEquals(31, count($testclass->eval_cron_field('1,*/2', 0, 59))); $this->assertEquals(15, count($testclass->eval_cron_field('1-10,5-15', 0, 59))); $this->assertEquals(13, count($testclass->eval_cron_field('1-10,5-15/2', 0, 59))); $this->assertEquals(3, count($testclass->eval_cron_field('1,2,3,1,2,3', 0, 59))); $this->assertEquals(1, count($testclass->eval_cron_field('-1,10,80', 0, 59))); } public function test_get_next_scheduled_time() { global $CFG; $this->resetAfterTest(); $this->setTimezone('Europe/London'); // Test job run at 1 am. $testclass = new \core\task\scheduled_test_task(); // All fields default to '*'. $testclass->set_hour('1'); $testclass->set_minute('0'); // Next valid time should be 1am of the next day. $nexttime = $testclass->get_next_scheduled_time(); $oneamdate = new DateTime('now', new DateTimeZone('Europe/London')); $oneamdate->setTime(1, 0, 0); // Make it 1 am tomorrow if the time is after 1am. if ($oneamdate->getTimestamp() < time()) { $oneamdate->add(new DateInterval('P1D')); } $oneam = $oneamdate->getTimestamp(); $this->assertEquals($oneam, $nexttime, 'Next scheduled time is 1am.'); // Disabled flag does not affect next time. $testclass->set_disabled(true); $nexttime = $testclass->get_next_scheduled_time(); $this->assertEquals($oneam, $nexttime, 'Next scheduled time is 1am.'); // Now test for job run every 10 minutes. $testclass = new \core\task\scheduled_test_task(); // All fields default to '*'. $testclass->set_minute('*/10'); // Next valid time should be next 10 minute boundary. $nexttime = $testclass->get_next_scheduled_time(); $minutes = ((intval(date('i') / 10))+1) * 10; $nexttenminutes = mktime(date('H'), $minutes, 0); $this->assertEquals($nexttenminutes, $nexttime, 'Next scheduled time is in 10 minutes.'); // Disabled flag does not affect next time. $testclass->set_disabled(true); $nexttime = $testclass->get_next_scheduled_time(); $this->assertEquals($nexttenminutes, $nexttime, 'Next scheduled time is in 10 minutes.'); // Test hourly job executed on Sundays only. $testclass = new \core\task\scheduled_test_task(); $testclass->set_minute('0'); $testclass->set_day_of_week('7'); $nexttime = $testclass->get_next_scheduled_time(); $this->assertEquals(7, date('N', $nexttime)); $this->assertEquals(0, date('i', $nexttime)); // Test monthly job $testclass = new \core\task\scheduled_test_task(); $testclass->set_minute('32'); $testclass->set_hour('0'); $testclass->set_day('1'); $nexttime = $testclass->get_next_scheduled_time(); $this->assertEquals(32, date('i', $nexttime)); $this->assertEquals(0, date('G', $nexttime)); $this->assertEquals(1, date('j', $nexttime)); } public function test_timezones() { global $CFG, $USER; // The timezones used in this test are chosen because they do not use DST - that would break the test. $this->resetAfterTest(); $this->setTimezone('Asia/Kabul'); $testclass = new \core\task\scheduled_test_task(); // Scheduled tasks should always use servertime - so this is 03:30 GMT. $testclass->set_hour('1'); $testclass->set_minute('0'); // Next valid time should be 1am of the next day. $nexttime = $testclass->get_next_scheduled_time(); // GMT+05:45. $USER->timezone = 'Asia/Kathmandu'; $userdate = userdate($nexttime); // Should be displayed in user timezone. // I used http://www.timeanddate.com/worldclock/fixedtime.html?msg=Moodle+Test&iso=20160502T01&p1=113 // setting my location to Kathmandu to verify this time. $this->assertContains('2:15 AM', core_text::strtoupper($userdate)); } public function test_reset_scheduled_tasks_for_component() { global $DB; $this->resetAfterTest(true); // Remember the defaults. $defaulttasks = \core\task\manager::load_scheduled_tasks_for_component('moodle'); $initcount = count($defaulttasks); // Customise a task. $firsttask = reset($defaulttasks); $firsttask->set_minute('1'); $firsttask->set_hour('2'); $firsttask->set_month('3'); $firsttask->set_day_of_week('4'); $firsttask->set_day('5'); $firsttask->set_customised('1'); \core\task\manager::configure_scheduled_task($firsttask); $firsttaskrecord = \core\task\manager::record_from_scheduled_task($firsttask); // We reset this field, because we do not want to compare it. $firsttaskrecord->nextruntime = '0'; // Delete a task to simulate the fact that its new. $secondtask = next($defaulttasks); $DB->delete_records('task_scheduled', array('classname' => '\\' . trim(get_class($secondtask), '\\'))); $this->assertFalse(\core\task\manager::get_scheduled_task(get_class($secondtask))); // Edit a task to simulate a change in its definition (as if it was not customised). $thirdtask = next($defaulttasks); $thirdtask->set_minute('1'); $thirdtask->set_hour('2'); $thirdtask->set_month('3'); $thirdtask->set_day_of_week('4'); $thirdtask->set_day('5'); $thirdtaskbefore = \core\task\manager::get_scheduled_task(get_class($thirdtask)); $thirdtaskbefore->set_next_run_time(null); // Ignore this value when comparing. \core\task\manager::configure_scheduled_task($thirdtask); $thirdtask = \core\task\manager::get_scheduled_task(get_class($thirdtask)); $thirdtask->set_next_run_time(null); // Ignore this value when comparing. $this->assertNotEquals($thirdtaskbefore, $thirdtask); // Now call reset on all the tasks. \core\task\manager::reset_scheduled_tasks_for_component('moodle'); // Load the tasks again. $defaulttasks = \core\task\manager::load_scheduled_tasks_for_component('moodle'); $finalcount = count($defaulttasks); // Compare the first task. $newfirsttask = reset($defaulttasks); $newfirsttaskrecord = \core\task\manager::record_from_scheduled_task($newfirsttask); // We reset this field, because we do not want to compare it. $newfirsttaskrecord->nextruntime = '0'; // Assert a customised task was not altered by reset. $this->assertEquals($firsttaskrecord, $newfirsttaskrecord); // Assert that the second task was added back. $secondtaskafter = \core\task\manager::get_scheduled_task(get_class($secondtask)); $secondtaskafter->set_next_run_time(null); // Do not compare the nextruntime. $secondtask->set_next_run_time(null); $this->assertEquals($secondtask, $secondtaskafter); // Assert that the third task edits were overridden. $thirdtaskafter = \core\task\manager::get_scheduled_task(get_class($thirdtask)); $thirdtaskafter->set_next_run_time(null); $this->assertEquals($thirdtaskbefore, $thirdtaskafter); // Assert we have the same number of tasks. $this->assertEquals($initcount, $finalcount); } /** * Tests that the reset function deletes old tasks. */ public function test_reset_scheduled_tasks_for_component_delete() { global $DB; $this->resetAfterTest(true); $count = $DB->count_records('task_scheduled', array('component' => 'moodle')); $allcount = $DB->count_records('task_scheduled'); $task = new \core\task\scheduled_test_task(); $task->set_component('moodle'); $record = \core\task\manager::record_from_scheduled_task($task); $DB->insert_record('task_scheduled', $record); $this->assertTrue($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test_task', 'component' => 'moodle'))); $task = new \core\task\scheduled_test2_task(); $task->set_component('moodle'); $record = \core\task\manager::record_from_scheduled_task($task); $DB->insert_record('task_scheduled', $record); $this->assertTrue($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test2_task', 'component' => 'moodle'))); $aftercount = $DB->count_records('task_scheduled', array('component' => 'moodle')); $afterallcount = $DB->count_records('task_scheduled'); $this->assertEquals($count + 2, $aftercount); $this->assertEquals($allcount + 2, $afterallcount); // Now check that the right things were deleted. \core\task\manager::reset_scheduled_tasks_for_component('moodle'); $this->assertEquals($count, $DB->count_records('task_scheduled', array('component' => 'moodle'))); $this->assertEquals($allcount, $DB->count_records('task_scheduled')); $this->assertFalse($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test2_task', 'component' => 'moodle'))); $this->assertFalse($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test_task', 'component' => 'moodle'))); } public function test_get_next_scheduled_task() { global $DB; $this->resetAfterTest(true); // Delete all existing scheduled tasks. $DB->delete_records('task_scheduled'); // Add a scheduled task. // A task that runs once per hour. $record = new stdClass(); $record->blocking = true; $record->minute = '0'; $record->hour = '0'; $record->dayofweek = '*'; $record->day = '*'; $record->month = '*'; $record->component = 'test_scheduled_task'; $record->classname = '\core\task\scheduled_test_task'; $DB->insert_record('task_scheduled', $record); // And another one to test failures. $record->classname = '\core\task\scheduled_test2_task'; $DB->insert_record('task_scheduled', $record); // And disabled test. $record->classname = '\core\task\scheduled_test3_task'; $record->disabled = 1; $DB->insert_record('task_scheduled', $record); $now = time(); // Should get handed the first task. $task = \core\task\manager::get_next_scheduled_task($now); $this->assertInstanceOf('\core\task\scheduled_test_task', $task); $task->execute(); \core\task\manager::scheduled_task_complete($task); // Should get handed the second task. $task = \core\task\manager::get_next_scheduled_task($now); $this->assertInstanceOf('\core\task\scheduled_test2_task', $task); $task->execute(); \core\task\manager::scheduled_task_failed($task); // Should not get any task. $task = \core\task\manager::get_next_scheduled_task($now); $this->assertNull($task); // Should get the second task (retry after delay). $task = \core\task\manager::get_next_scheduled_task($now + 120); $this->assertInstanceOf('\core\task\scheduled_test2_task', $task); $task->execute(); \core\task\manager::scheduled_task_complete($task); // Should not get any task. $task = \core\task\manager::get_next_scheduled_task($now); $this->assertNull($task); // Check ordering. $DB->delete_records('task_scheduled'); $record->lastruntime = 2; $record->disabled = 0; $record->classname = '\core\task\scheduled_test_task'; $DB->insert_record('task_scheduled', $record); $record->lastruntime = 1; $record->classname = '\core\task\scheduled_test2_task'; $DB->insert_record('task_scheduled', $record); // Should get handed the second task. $task = \core\task\manager::get_next_scheduled_task($now); $this->assertInstanceOf('\core\task\scheduled_test2_task', $task); $task->execute(); \core\task\manager::scheduled_task_complete($task); // Should get handed the first task. $task = \core\task\manager::get_next_scheduled_task($now); $this->assertInstanceOf('\core\task\scheduled_test_task', $task); $task->execute(); \core\task\manager::scheduled_task_complete($task); // Should not get any task. $task = \core\task\manager::get_next_scheduled_task($now); $this->assertNull($task); } public function test_get_broken_scheduled_task() { global $DB; $this->resetAfterTest(true); // Delete all existing scheduled tasks. $DB->delete_records('task_scheduled'); // Add a scheduled task. // A broken task that runs all the time. $record = new stdClass(); $record->blocking = true; $record->minute = '*'; $record->hour = '*'; $record->dayofweek = '*'; $record->day = '*'; $record->month = '*'; $record->component = 'test_scheduled_task'; $record->classname = '\core\task\scheduled_test_task_broken'; $DB->insert_record('task_scheduled', $record); $now = time(); // Should not get any task. $task = \core\task\manager::get_next_scheduled_task($now); $this->assertDebuggingCalled(); $this->assertNull($task); } /** * Tests the use of 'R' syntax in time fields of tasks to get * tasks be configured with a non-uniform time. */ public function test_random_time_specification() { // Testing non-deterministic things in a unit test is not really // wise, so we just test the values have changed within allowed bounds. $testclass = new \core\task\scheduled_test_task(); // The test task defaults to '*'. $this->assertInternalType('string', $testclass->get_minute()); $this->assertInternalType('string', $testclass->get_hour()); // Set a random value. $testclass->set_minute('R'); $testclass->set_hour('R'); $testclass->set_day_of_week('R'); // Verify the minute has changed within allowed bounds. $minute = $testclass->get_minute(); $this->assertInternalType('int', $minute); $this->assertGreaterThanOrEqual(0, $minute); $this->assertLessThanOrEqual(59, $minute); // Verify the hour has changed within allowed bounds. $hour = $testclass->get_hour(); $this->assertInternalType('int', $hour); $this->assertGreaterThanOrEqual(0, $hour); $this->assertLessThanOrEqual(23, $hour); // Verify the dayofweek has changed within allowed bounds. $dayofweek = $testclass->get_day_of_week(); $this->assertInternalType('int', $dayofweek); $this->assertGreaterThanOrEqual(0, $dayofweek); $this->assertLessThanOrEqual(6, $dayofweek); } /** * Test that the file_temp_cleanup_task removes directories and * files as expected. */ public function test_file_temp_cleanup_task() { global $CFG; $backuptempdir = make_backup_temp_directory(''); // Create directories. $dir = $backuptempdir . DIRECTORY_SEPARATOR . 'backup01' . DIRECTORY_SEPARATOR . 'courses'; mkdir($dir, 0777, true); // Create files to be checked and then deleted. $file01 = $dir . DIRECTORY_SEPARATOR . 'sections.xml'; file_put_contents($file01, 'test data 001'); $file02 = $dir . DIRECTORY_SEPARATOR . 'modules.xml'; file_put_contents($file02, 'test data 002'); // Change the time modified for the first file, to a time that will be deleted by the task (greater than seven days). touch($file01, time() - (8 * 24 * 3600)); $task = \core\task\manager::get_scheduled_task('\\core\\task\\file_temp_cleanup_task'); $this->assertInstanceOf('\core\task\file_temp_cleanup_task', $task); $task->execute(); // Scan the directory. Only modules.xml should be left. $filesarray = scandir($dir); $this->assertEquals('modules.xml', $filesarray[2]); $this->assertEquals(3, count($filesarray)); // Change the time modified on modules.xml. touch($file02, time() - (8 * 24 * 3600)); // Change the time modified on the courses directory. touch($backuptempdir . DIRECTORY_SEPARATOR . 'backup01' . DIRECTORY_SEPARATOR . 'courses', time() - (8 * 24 * 3600)); // Run the scheduled task to remove the file and directory. $task->execute(); $filesarray = scandir($backuptempdir . DIRECTORY_SEPARATOR . 'backup01'); // There should only be two items in the array, '.' and '..'. $this->assertEquals(2, count($filesarray)); // Change the time modified on all of the files and directories. $dir = new \RecursiveDirectoryIterator($CFG->tempdir); // Show all child nodes prior to their parent. $iter = new \RecursiveIteratorIterator($dir, \RecursiveIteratorIterator::CHILD_FIRST); for ($iter->rewind(); $iter->valid(); $iter->next()) { if ($iter->isDir() && !$iter->isDot()) { $node = $iter->getRealPath(); touch($node, time() - (8 * 24 * 3600)); } } // Run the scheduled task again to remove all of the files and directories. $task->execute(); $filesarray = scandir($CFG->tempdir); // All of the files and directories should be deleted. // There should only be three items in the array, '.', '..' and '.htaccess'. $this->assertEquals([ '.', '..', '.htaccess' ], $filesarray); } /** * Test that the function to clear the fail delay from a task works correctly. */ public function test_clear_fail_delay() { $this->resetAfterTest(); // Get an example task to use for testing. Task is set to run every minute by default. $taskname = '\core\task\send_new_user_passwords_task'; // Pretend task started running and then failed 3 times. $before = time(); $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron'); for ($i = 0; $i < 3; $i ++) { $task = \core\task\manager::get_scheduled_task($taskname); $lock = $cronlockfactory->get_lock('\\' . get_class($task), 10); $task->set_lock($lock); \core\task\manager::scheduled_task_failed($task); } // Confirm task is now delayed by several minutes. $task = \core\task\manager::get_scheduled_task($taskname); $this->assertEquals(240, $task->get_fail_delay()); $this->assertGreaterThan($before + 230, $task->get_next_run_time()); // Clear the fail delay and re-get the task. \core\task\manager::clear_fail_delay($task); $task = \core\task\manager::get_scheduled_task($taskname); // There should be no delay and it should run within the next minute. $this->assertEquals(0, $task->get_fail_delay()); $this->assertLessThan($before + 70, $task->get_next_run_time()); } }