Merge branch 'MDL-47480-master' of git://github.com/ankitagarwal/moodle
[moodle.git] / lib / tests / scheduled_task_test.php
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/>.
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  */
26 defined('MOODLE_INTERNAL') || die();
27 require_once(__DIR__ . '/fixtures/task_fixtures.php');
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 {
39     /**
40      * Test the cron scheduling method
41      */
42     public function test_eval_cron_field() {
43         $testclass = new \core\task\scheduled_test_task();
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     }
53     public function test_get_next_scheduled_time() {
54         global $CFG;
55         $this->resetAfterTest();
57         $this->setTimezone('Europe/London');
59         // Test job run at 1 am.
60         $testclass = new \core\task\scheduled_test_task();
62         // All fields default to '*'.
63         $testclass->set_hour('1');
64         $testclass->set_minute('0');
65         // Next valid time should be 1am of the next day.
66         $nexttime = $testclass->get_next_scheduled_time();
68         $oneamdate = new DateTime('now', new DateTimeZone('Europe/London'));
69         $oneamdate->setTime(1, 0, 0);
70         // Make it 1 am tomorrow if the time is after 1am.
71         if ($oneamdate->getTimestamp() < time()) {
72             $oneamdate->add(new DateInterval('P1D'));
73         }
74         $oneam = $oneamdate->getTimestamp();
76         $this->assertEquals($oneam, $nexttime, 'Next scheduled time is 1am.');
78         // Disabled flag does not affect next time.
79         $testclass->set_disabled(true);
80         $nexttime = $testclass->get_next_scheduled_time();
81         $this->assertEquals($oneam, $nexttime, 'Next scheduled time is 1am.');
83         // Now test for job run every 10 minutes.
84         $testclass = new \core\task\scheduled_test_task();
86         // All fields default to '*'.
87         $testclass->set_minute('*/10');
88         // Next valid time should be next 10 minute boundary.
89         $nexttime = $testclass->get_next_scheduled_time();
91         $minutes = ((intval(date('i') / 10))+1) * 10;
92         $nexttenminutes = mktime(date('H'), $minutes, 0);
94         $this->assertEquals($nexttenminutes, $nexttime, 'Next scheduled time is in 10 minutes.');
96         // Disabled flag does not affect next time.
97         $testclass->set_disabled(true);
98         $nexttime = $testclass->get_next_scheduled_time();
99         $this->assertEquals($nexttenminutes, $nexttime, 'Next scheduled time is in 10 minutes.');
101         // Test hourly job executed on Sundays only.
102         $testclass = new \core\task\scheduled_test_task();
103         $testclass->set_minute('0');
104         $testclass->set_day_of_week('7');
106         $nexttime = $testclass->get_next_scheduled_time();
108         $this->assertEquals(7, date('N', $nexttime));
109         $this->assertEquals(0, date('i', $nexttime));
111         // Test monthly job
112         $testclass = new \core\task\scheduled_test_task();
113         $testclass->set_minute('32');
114         $testclass->set_hour('0');
115         $testclass->set_day('1');
117         $nexttime = $testclass->get_next_scheduled_time();
119         $this->assertEquals(32, date('i', $nexttime));
120         $this->assertEquals(0, date('G', $nexttime));
121         $this->assertEquals(1, date('j', $nexttime));
122     }
124     public function test_timezones() {
125         global $CFG, $USER;
127         // The timezones used in this test are chosen because they do not use DST - that would break the test.
128         $this->resetAfterTest();
130         $this->setTimezone('America/Caracas');
132         $testclass = new \core\task\scheduled_test_task();
134         // Scheduled tasks should always use servertime - so this is 03:30 GMT.
135         $testclass->set_hour('1');
136         $testclass->set_minute('0');
138         // Next valid time should be 1am of the next day.
139         $nexttime = $testclass->get_next_scheduled_time();
141         // GMT+05:45.
142         $USER->timezone = 'Asia/Kathmandu';
143         $userdate = userdate($nexttime);
145         // Should be displayed in user timezone.
146         // I used http://www.timeanddate.com/worldclock/fixedtime.html?msg=Moodle+Test&iso=20140314T01&p1=58
147         // to verify this time.
148         $this->assertContains('11:15 AM', core_text::strtoupper($userdate));
149     }
151     public function test_reset_scheduled_tasks_for_component() {
152         global $DB;
154         $this->resetAfterTest(true);
155         // Remember the defaults.
156         $defaulttasks = \core\task\manager::load_scheduled_tasks_for_component('moodle');
157         $initcount = count($defaulttasks);
158         // Customise a task.
159         $firsttask = reset($defaulttasks);
160         $firsttask->set_minute('1');
161         $firsttask->set_hour('2');
162         $firsttask->set_month('3');
163         $firsttask->set_day_of_week('4');
164         $firsttask->set_day('5');
165         $firsttask->set_customised('1');
166         \core\task\manager::configure_scheduled_task($firsttask);
167         $firsttaskrecord = \core\task\manager::record_from_scheduled_task($firsttask);
168         // We reset this field, because we do not want to compare it.
169         $firsttaskrecord->nextruntime = '0';
171         // Delete a task to simulate the fact that its new.
172         $secondtask = next($defaulttasks);
173         $DB->delete_records('task_scheduled', array('classname' => '\\' . trim(get_class($secondtask), '\\')));
174         $this->assertFalse(\core\task\manager::get_scheduled_task(get_class($secondtask)));
176         // Edit a task to simulate a change in its definition (as if it was not customised).
177         $thirdtask = next($defaulttasks);
178         $thirdtask->set_minute('1');
179         $thirdtask->set_hour('2');
180         $thirdtask->set_month('3');
181         $thirdtask->set_day_of_week('4');
182         $thirdtask->set_day('5');
183         $thirdtaskbefore = \core\task\manager::get_scheduled_task(get_class($thirdtask));
184         $thirdtaskbefore->set_next_run_time(null);      // Ignore this value when comparing.
185         \core\task\manager::configure_scheduled_task($thirdtask);
186         $thirdtask = \core\task\manager::get_scheduled_task(get_class($thirdtask));
187         $thirdtask->set_next_run_time(null);            // Ignore this value when comparing.
188         $this->assertNotEquals($thirdtaskbefore, $thirdtask);
190         // Now call reset on all the tasks.
191         \core\task\manager::reset_scheduled_tasks_for_component('moodle');
193         // Load the tasks again.
194         $defaulttasks = \core\task\manager::load_scheduled_tasks_for_component('moodle');
195         $finalcount = count($defaulttasks);
196         // Compare the first task.
197         $newfirsttask = reset($defaulttasks);
198         $newfirsttaskrecord = \core\task\manager::record_from_scheduled_task($newfirsttask);
199         // We reset this field, because we do not want to compare it.
200         $newfirsttaskrecord->nextruntime = '0';
202         // Assert a customised task was not altered by reset.
203         $this->assertEquals($firsttaskrecord, $newfirsttaskrecord);
205         // Assert that the second task was added back.
206         $secondtaskafter = \core\task\manager::get_scheduled_task(get_class($secondtask));
207         $secondtaskafter->set_next_run_time(null);   // Do not compare the nextruntime.
208         $secondtask->set_next_run_time(null);
209         $this->assertEquals($secondtask, $secondtaskafter);
211         // Assert that the third task edits were overridden.
212         $thirdtaskafter = \core\task\manager::get_scheduled_task(get_class($thirdtask));
213         $thirdtaskafter->set_next_run_time(null);
214         $this->assertEquals($thirdtaskbefore, $thirdtaskafter);
216         // Assert we have the same number of tasks.
217         $this->assertEquals($initcount, $finalcount);
218     }
220     public function test_get_next_scheduled_task() {
221         global $DB;
223         $this->resetAfterTest(true);
224         // Delete all existing scheduled tasks.
225         $DB->delete_records('task_scheduled');
226         // Add a scheduled task.
228         // A task that runs once per hour.
229         $record = new stdClass();
230         $record->blocking = true;
231         $record->minute = '0';
232         $record->hour = '0';
233         $record->dayofweek = '*';
234         $record->day = '*';
235         $record->month = '*';
236         $record->component = 'test_scheduled_task';
237         $record->classname = '\core\task\scheduled_test_task';
239         $DB->insert_record('task_scheduled', $record);
240         // And another one to test failures.
241         $record->classname = '\core\task\scheduled_test2_task';
242         $DB->insert_record('task_scheduled', $record);
243         // And disabled test.
244         $record->classname = '\core\task\scheduled_test3_task';
245         $record->disabled = 1;
246         $DB->insert_record('task_scheduled', $record);
248         $now = time();
250         // Should get handed the first task.
251         $task = \core\task\manager::get_next_scheduled_task($now);
252         $this->assertInstanceOf('\core\task\scheduled_test_task', $task);
253         $task->execute();
255         \core\task\manager::scheduled_task_complete($task);
256         // Should get handed the second task.
257         $task = \core\task\manager::get_next_scheduled_task($now);
258         $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
259         $task->execute();
261         \core\task\manager::scheduled_task_failed($task);
262         // Should not get any task.
263         $task = \core\task\manager::get_next_scheduled_task($now);
264         $this->assertNull($task);
266         // Should get the second task (retry after delay).
267         $task = \core\task\manager::get_next_scheduled_task($now + 120);
268         $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
269         $task->execute();
271         \core\task\manager::scheduled_task_complete($task);
273         // Should not get any task.
274         $task = \core\task\manager::get_next_scheduled_task($now);
275         $this->assertNull($task);
277         // Check ordering.
278         $DB->delete_records('task_scheduled');
279         $record->lastruntime = 2;
280         $record->disabled = 0;
281         $record->classname = '\core\task\scheduled_test_task';
282         $DB->insert_record('task_scheduled', $record);
284         $record->lastruntime = 1;
285         $record->classname = '\core\task\scheduled_test2_task';
286         $DB->insert_record('task_scheduled', $record);
288         // Should get handed the second task.
289         $task = \core\task\manager::get_next_scheduled_task($now);
290         $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
291         $task->execute();
292         \core\task\manager::scheduled_task_complete($task);
294         // Should get handed the first task.
295         $task = \core\task\manager::get_next_scheduled_task($now);
296         $this->assertInstanceOf('\core\task\scheduled_test_task', $task);
297         $task->execute();
298         \core\task\manager::scheduled_task_complete($task);
300         // Should not get any task.
301         $task = \core\task\manager::get_next_scheduled_task($now);
302         $this->assertNull($task);
303     }
305     public function test_get_broken_scheduled_task() {
306         global $DB;
308         $this->resetAfterTest(true);
309         // Delete all existing scheduled tasks.
310         $DB->delete_records('task_scheduled');
311         // Add a scheduled task.
313         // A broken task that runs all the time.
314         $record = new stdClass();
315         $record->blocking = true;
316         $record->minute = '*';
317         $record->hour = '*';
318         $record->dayofweek = '*';
319         $record->day = '*';
320         $record->month = '*';
321         $record->component = 'test_scheduled_task';
322         $record->classname = '\core\task\scheduled_test_task_broken';
324         $DB->insert_record('task_scheduled', $record);
326         $now = time();
327         // Should not get any task.
328         $task = \core\task\manager::get_next_scheduled_task($now);
329         $this->assertDebuggingCalled();
330         $this->assertNull($task);
331     }
333     /**
334      * Tests the use of 'R' syntax in time fields of tasks to get
335      * tasks be configured with a non-uniform time.
336      */
337     public function test_random_time_specification() {
339         // Testing non-deterministic things in a unit test is not really
340         // wise, so we just test the values have changed within allowed bounds.
341         $testclass = new \core\task\scheduled_test_task();
343         // The test task defaults to '*'.
344         $this->assertInternalType('string', $testclass->get_minute());
345         $this->assertInternalType('string', $testclass->get_hour());
347         // Set a random value.
348         $testclass->set_minute('R');
349         $testclass->set_hour('R');
350         $testclass->set_day_of_week('R');
352         // Verify the minute has changed within allowed bounds.
353         $minute = $testclass->get_minute();
354         $this->assertInternalType('int', $minute);
355         $this->assertGreaterThanOrEqual(0, $minute);
356         $this->assertLessThanOrEqual(59, $minute);
358         // Verify the hour has changed within allowed bounds.
359         $hour = $testclass->get_hour();
360         $this->assertInternalType('int', $hour);
361         $this->assertGreaterThanOrEqual(0, $hour);
362         $this->assertLessThanOrEqual(23, $hour);
364         // Verify the dayofweek has changed within allowed bounds.
365         $dayofweek = $testclass->get_day_of_week();
366         $this->assertInternalType('int', $dayofweek);
367         $this->assertGreaterThanOrEqual(0, $dayofweek);
368         $this->assertLessThanOrEqual(6, $dayofweek);
369     }
371     /**
372      * Test that the file_temp_cleanup_task removes directories and
373      * files as expected.
374      */
375     public function test_file_temp_cleanup_task() {
376         global $CFG;
378         // Create directories.
379         $dir = $CFG->tempdir . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR . 'backup01' . DIRECTORY_SEPARATOR . 'courses';
380         mkdir($dir, 0777, true);
382         // Create files to be checked and then deleted.
383         $file01 = $dir . DIRECTORY_SEPARATOR . 'sections.xml';
384         file_put_contents($file01, 'test data 001');
385         $file02 = $dir . DIRECTORY_SEPARATOR . 'modules.xml';
386         file_put_contents($file02, 'test data 002');
387         // Change the time modified for the first file, to a time that will be deleted by the task (greater than seven days).
388         touch($file01, time() - (8 * 24 * 3600));
390         $task = \core\task\manager::get_scheduled_task('\\core\\task\\file_temp_cleanup_task');
391         $this->assertInstanceOf('\core\task\file_temp_cleanup_task', $task);
392         $task->execute();
394         // Scan the directory. Only modules.xml should be left.
395         $filesarray = scandir($dir);
396         $this->assertEquals('modules.xml', $filesarray[2]);
397         $this->assertEquals(3, count($filesarray));
399         // Change the time modified on modules.xml.
400         touch($file02, time() - (8 * 24 * 3600));
401         // Change the time modified on the courses directory.
402         touch($CFG->tempdir . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR . 'backup01' . DIRECTORY_SEPARATOR .
403                 'courses', time() - (8 * 24 * 3600));
404         // Run the scheduled task to remove the file and directory.
405         $task->execute();
406         $filesarray = scandir($CFG->tempdir . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR . 'backup01');
407         // There should only be two items in the array, '.' and '..'.
408         $this->assertEquals(2, count($filesarray));
410         // Change the time modified on all of the files and directories.
411         $dir = new \RecursiveDirectoryIterator($CFG->tempdir);
412         // Show all child nodes prior to their parent.
413         $iter = new \RecursiveIteratorIterator($dir, \RecursiveIteratorIterator::CHILD_FIRST);
415         for ($iter->rewind(); $iter->valid(); $iter->next()) {
416             if ($iter->isDir() && !$iter->isDot()) {
417                 $node = $iter->getRealPath();
418                 touch($node, time() - (8 * 24 * 3600));
419             }
420         }
422         // Run the scheduled task again to remove all of the files and directories.
423         $task->execute();
424         $filesarray = scandir($CFG->tempdir);
425         // All of the files and directories should be deleted.
426         // There should only be two items in the array, '.' and '..'.
427         $this->assertEquals(2, count($filesarray));
428     }