Merge branch 'MDL-47480-master' of git://github.com/ankitagarwal/moodle
[moodle.git] / lib / tests / scheduled_task_test.php
CommitLineData
309ae892
DW
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/**
0a5aa65b 18 * This file contains the unittests for scheduled tasks.
309ae892
DW
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
26defined('MOODLE_INTERNAL') || die();
0a5aa65b 27require_once(__DIR__ . '/fixtures/task_fixtures.php');
309ae892
DW
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 */
0a5aa65b 37class core_scheduled_task_testcase extends advanced_testcase {
309ae892
DW
38
39 /**
40 * Test the cron scheduling method
41 */
42 public function test_eval_cron_field() {
0a5aa65b 43 $testclass = new \core\task\scheduled_test_task();
309ae892
DW
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() {
d6e7a63d
PS
54 global $CFG;
55 $this->resetAfterTest();
56
57 $this->setTimezone('Europe/London');
58
309ae892 59 // Test job run at 1 am.
0a5aa65b 60 $testclass = new \core\task\scheduled_test_task();
309ae892
DW
61
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();
67
d6e7a63d
PS
68 $oneamdate = new DateTime('now', new DateTimeZone('Europe/London'));
69 $oneamdate->setTime(1, 0, 0);
309ae892 70 // Make it 1 am tomorrow if the time is after 1am.
d6e7a63d
PS
71 if ($oneamdate->getTimestamp() < time()) {
72 $oneamdate->add(new DateInterval('P1D'));
309ae892 73 }
d6e7a63d 74 $oneam = $oneamdate->getTimestamp();
309ae892
DW
75
76 $this->assertEquals($oneam, $nexttime, 'Next scheduled time is 1am.');
77
0a5aa65b
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.');
82
309ae892 83 // Now test for job run every 10 minutes.
0a5aa65b 84 $testclass = new \core\task\scheduled_test_task();
309ae892
DW
85
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();
90
91 $minutes = ((intval(date('i') / 10))+1) * 10;
92 $nexttenminutes = mktime(date('H'), $minutes, 0);
93
94 $this->assertEquals($nexttenminutes, $nexttime, 'Next scheduled time is in 10 minutes.');
0a5aa65b
95
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.');
dc561732
DM
100
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');
105
106 $nexttime = $testclass->get_next_scheduled_time();
107
108 $this->assertEquals(7, date('N', $nexttime));
109 $this->assertEquals(0, date('i', $nexttime));
110
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');
116
117 $nexttime = $testclass->get_next_scheduled_time();
118
119 $this->assertEquals(32, date('i', $nexttime));
120 $this->assertEquals(0, date('G', $nexttime));
121 $this->assertEquals(1, date('j', $nexttime));
309ae892
DW
122 }
123
bbd9226c
DW
124 public function test_timezones() {
125 global $CFG, $USER;
126
127 // The timezones used in this test are chosen because they do not use DST - that would break the test.
d6e7a63d 128 $this->resetAfterTest();
bbd9226c 129
d6e7a63d 130 $this->setTimezone('America/Caracas');
bbd9226c 131
0a5aa65b 132 $testclass = new \core\task\scheduled_test_task();
bbd9226c
DW
133
134 // Scheduled tasks should always use servertime - so this is 03:30 GMT.
135 $testclass->set_hour('1');
136 $testclass->set_minute('0');
137
138 // Next valid time should be 1am of the next day.
139 $nexttime = $testclass->get_next_scheduled_time();
140
141 // GMT+05:45.
142 $USER->timezone = 'Asia/Kathmandu';
143 $userdate = userdate($nexttime);
144
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.
9be8583a 148 $this->assertContains('11:15 AM', core_text::strtoupper($userdate));
bbd9226c
DW
149 }
150
852ff037
DW
151 public function test_reset_scheduled_tasks_for_component() {
152 global $DB;
153
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';
170
0af336ef
FM
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)));
175
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);
189
852ff037
DW
190 // Now call reset on all the tasks.
191 \core\task\manager::reset_scheduled_tasks_for_component('moodle');
192
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';
201
202 // Assert a customised task was not altered by reset.
203 $this->assertEquals($firsttaskrecord, $newfirsttaskrecord);
204
0af336ef
FM
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);
210
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);
215
852ff037
DW
216 // Assert we have the same number of tasks.
217 $this->assertEquals($initcount, $finalcount);
218 }
219
309ae892
DW
220 public function test_get_next_scheduled_task() {
221 global $DB;
222
223 $this->resetAfterTest(true);
224 // Delete all existing scheduled tasks.
225 $DB->delete_records('task_scheduled');
226 // Add a scheduled task.
227
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';
0a5aa65b 237 $record->classname = '\core\task\scheduled_test_task';
309ae892
DW
238
239 $DB->insert_record('task_scheduled', $record);
240 // And another one to test failures.
0a5aa65b
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;
309ae892 246 $DB->insert_record('task_scheduled', $record);
0a5aa65b 247
309ae892
DW
248 $now = time();
249
250 // Should get handed the first task.
251 $task = \core\task\manager::get_next_scheduled_task($now);
0a5aa65b 252 $this->assertInstanceOf('\core\task\scheduled_test_task', $task);
309ae892
DW
253 $task->execute();
254
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);
0a5aa65b 258 $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
309ae892
DW
259 $task->execute();
260
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);
265
266 // Should get the second task (retry after delay).
267 $task = \core\task\manager::get_next_scheduled_task($now + 120);
0a5aa65b 268 $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
309ae892
DW
269 $task->execute();
270
271 \core\task\manager::scheduled_task_complete($task);
272
273 // Should not get any task.
274 $task = \core\task\manager::get_next_scheduled_task($now);
275 $this->assertNull($task);
c44b4213
AA
276
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);
283
284 $record->lastruntime = 1;
285 $record->classname = '\core\task\scheduled_test2_task';
286 $DB->insert_record('task_scheduled', $record);
287
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);
293
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);
299
300 // Should not get any task.
301 $task = \core\task\manager::get_next_scheduled_task($now);
302 $this->assertNull($task);
309ae892 303 }
a0ac4060
DW
304
305 public function test_get_broken_scheduled_task() {
306 global $DB;
307
308 $this->resetAfterTest(true);
309 // Delete all existing scheduled tasks.
310 $DB->delete_records('task_scheduled');
311 // Add a scheduled task.
312
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';
323
324 $DB->insert_record('task_scheduled', $record);
325
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 }
16078807
DP
332
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() {
338
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();
342
343 // The test task defaults to '*'.
344 $this->assertInternalType('string', $testclass->get_minute());
345 $this->assertInternalType('string', $testclass->get_hour());
346
347 // Set a random value.
348 $testclass->set_minute('R');
349 $testclass->set_hour('R');
f47e4eb4 350 $testclass->set_day_of_week('R');
16078807
DP
351
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);
357
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);
f47e4eb4
AD
363
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);
16078807 369 }
b49964f7
AG
370
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;
377
378 // Create directories.
379 $dir = $CFG->tempdir . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR . 'backup01' . DIRECTORY_SEPARATOR . 'courses';
380 mkdir($dir, 0777, true);
381
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));
389
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();
393
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));
398
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));
409
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);
414
415 for ($iter->rewind(); $iter->valid(); $iter->next()) {
d83a1c11
RT
416 if ($iter->isDir() && !$iter->isDot()) {
417 $node = $iter->getRealPath();
418 touch($node, time() - (8 * 24 * 3600));
419 }
b49964f7
AG
420 }
421
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 }
309ae892 429}