MDL-69419 core: Task reset tests cannot use random test times
[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
dd215cb1 130 $this->setTimezone('Asia/Kabul');
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.
dd215cb1
DM
146 // I used http://www.timeanddate.com/worldclock/fixedtime.html?msg=Moodle+Test&iso=20160502T01&p1=113
147 // setting my location to Kathmandu to verify this time.
148 $this->assertContains('2:15 AM', core_text::strtoupper($userdate));
bbd9226c
DW
149 }
150
0400b5ef 151 public function test_reset_scheduled_tasks_for_component_customised(): void {
852ff037 152 $this->resetAfterTest(true);
0400b5ef
AN
153
154 $tasks = \core\task\manager::load_scheduled_tasks_for_component('moodle');
155
852ff037 156 // Customise a task.
0400b5ef
AN
157 $task = reset($tasks);
158 $task->set_minute('1');
159 $task->set_hour('2');
160 $task->set_month('3');
161 $task->set_day_of_week('4');
162 $task->set_day('5');
163 $task->set_customised('1');
164 \core\task\manager::configure_scheduled_task($task);
165
166 // Now call reset.
167 \core\task\manager::reset_scheduled_tasks_for_component('moodle');
168
169 // Fetch the task again.
170 $taskafterreset = \core\task\manager::get_scheduled_task(get_class($task));
171
172 // The task should still be the same as the customised.
173 $this->assertTaskEquals($task, $taskafterreset);
174 }
175
176 public function test_reset_scheduled_tasks_for_component_deleted(): void {
177 global $DB;
178 $this->resetAfterTest(true);
852ff037 179
0af336ef 180 // Delete a task to simulate the fact that its new.
0400b5ef 181 $tasklist = \core\task\manager::load_scheduled_tasks_for_component('moodle');
0af336ef 182
0400b5ef
AN
183 // Note: This test must use a task which does not use any random values.
184 $task = \core\task\manager::get_scheduled_task(core\task\session_cleanup_task::class);
185
186 $DB->delete_records('task_scheduled', array('classname' => '\\' . trim(get_class($task), '\\')));
187 $this->assertFalse(\core\task\manager::get_scheduled_task(core\task\session_cleanup_task::class));
0af336ef 188
852ff037
DW
189 // Now call reset on all the tasks.
190 \core\task\manager::reset_scheduled_tasks_for_component('moodle');
191
0400b5ef
AN
192 // Assert that the second task was added back.
193 $taskafterreset = \core\task\manager::get_scheduled_task(core\task\session_cleanup_task::class);
194 $this->assertNotFalse($taskafterreset);
852ff037 195
0400b5ef
AN
196 $this->assertTaskEquals($task, $taskafterreset);
197 $this->assertCount(count($tasklist), \core\task\manager::load_scheduled_tasks_for_component('moodle'));
198 }
852ff037 199
0400b5ef
AN
200 public function test_reset_scheduled_tasks_for_component_changed_in_source(): void {
201 $this->resetAfterTest(true);
202
203 // Delete a task to simulate the fact that its new.
204 // Note: This test must use a task which does not use any random values.
205 $task = \core\task\manager::get_scheduled_task(core\task\session_cleanup_task::class);
206
207 // Get a copy of the task before maing changes for later comparison.
208 $taskbeforechange = \core\task\manager::get_scheduled_task(core\task\session_cleanup_task::class);
209
210 // Edit a task to simulate a change in its definition (as if it was not customised).
211 $task->set_minute('1');
212 $task->set_hour('2');
213 $task->set_month('3');
214 $task->set_day_of_week('4');
215 $task->set_day('5');
216 \core\task\manager::configure_scheduled_task($task);
217
218 // Fetch the task out for comparison.
219 $taskafterchange = \core\task\manager::get_scheduled_task(core\task\session_cleanup_task::class);
220
221 // The task should now be different to the original.
222 $this->assertTaskNotEquals($taskbeforechange, $taskafterchange);
223
224 // Now call reset.
225 \core\task\manager::reset_scheduled_tasks_for_component('moodle');
226
227 // Fetch the task again.
228 $taskafterreset = \core\task\manager::get_scheduled_task(core\task\session_cleanup_task::class);
229
230 // The task should now be the same as the original.
231 $this->assertTaskEquals($taskbeforechange, $taskafterreset);
852ff037
DW
232 }
233
b2f6f88d
FM
234 /**
235 * Tests that the reset function deletes old tasks.
236 */
237 public function test_reset_scheduled_tasks_for_component_delete() {
238 global $DB;
239 $this->resetAfterTest(true);
240
241 $count = $DB->count_records('task_scheduled', array('component' => 'moodle'));
242 $allcount = $DB->count_records('task_scheduled');
243
244 $task = new \core\task\scheduled_test_task();
245 $task->set_component('moodle');
246 $record = \core\task\manager::record_from_scheduled_task($task);
247 $DB->insert_record('task_scheduled', $record);
248 $this->assertTrue($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test_task',
249 'component' => 'moodle')));
250
251 $task = new \core\task\scheduled_test2_task();
252 $task->set_component('moodle');
253 $record = \core\task\manager::record_from_scheduled_task($task);
254 $DB->insert_record('task_scheduled', $record);
255 $this->assertTrue($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test2_task',
256 'component' => 'moodle')));
257
258 $aftercount = $DB->count_records('task_scheduled', array('component' => 'moodle'));
259 $afterallcount = $DB->count_records('task_scheduled');
260
261 $this->assertEquals($count + 2, $aftercount);
262 $this->assertEquals($allcount + 2, $afterallcount);
263
264 // Now check that the right things were deleted.
265 \core\task\manager::reset_scheduled_tasks_for_component('moodle');
266
267 $this->assertEquals($count, $DB->count_records('task_scheduled', array('component' => 'moodle')));
268 $this->assertEquals($allcount, $DB->count_records('task_scheduled'));
269 $this->assertFalse($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test2_task',
270 'component' => 'moodle')));
271 $this->assertFalse($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test_task',
272 'component' => 'moodle')));
273 }
274
309ae892
DW
275 public function test_get_next_scheduled_task() {
276 global $DB;
277
278 $this->resetAfterTest(true);
279 // Delete all existing scheduled tasks.
280 $DB->delete_records('task_scheduled');
281 // Add a scheduled task.
282
283 // A task that runs once per hour.
284 $record = new stdClass();
285 $record->blocking = true;
286 $record->minute = '0';
287 $record->hour = '0';
288 $record->dayofweek = '*';
289 $record->day = '*';
290 $record->month = '*';
291 $record->component = 'test_scheduled_task';
0a5aa65b 292 $record->classname = '\core\task\scheduled_test_task';
309ae892
DW
293
294 $DB->insert_record('task_scheduled', $record);
295 // And another one to test failures.
0a5aa65b
296 $record->classname = '\core\task\scheduled_test2_task';
297 $DB->insert_record('task_scheduled', $record);
298 // And disabled test.
299 $record->classname = '\core\task\scheduled_test3_task';
300 $record->disabled = 1;
309ae892 301 $DB->insert_record('task_scheduled', $record);
0a5aa65b 302
309ae892
DW
303 $now = time();
304
305 // Should get handed the first task.
306 $task = \core\task\manager::get_next_scheduled_task($now);
0a5aa65b 307 $this->assertInstanceOf('\core\task\scheduled_test_task', $task);
309ae892
DW
308 $task->execute();
309
310 \core\task\manager::scheduled_task_complete($task);
311 // Should get handed the second task.
312 $task = \core\task\manager::get_next_scheduled_task($now);
0a5aa65b 313 $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
309ae892
DW
314 $task->execute();
315
316 \core\task\manager::scheduled_task_failed($task);
317 // Should not get any task.
318 $task = \core\task\manager::get_next_scheduled_task($now);
319 $this->assertNull($task);
320
321 // Should get the second task (retry after delay).
322 $task = \core\task\manager::get_next_scheduled_task($now + 120);
0a5aa65b 323 $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
309ae892
DW
324 $task->execute();
325
326 \core\task\manager::scheduled_task_complete($task);
327
328 // Should not get any task.
329 $task = \core\task\manager::get_next_scheduled_task($now);
330 $this->assertNull($task);
c44b4213
AA
331
332 // Check ordering.
333 $DB->delete_records('task_scheduled');
334 $record->lastruntime = 2;
335 $record->disabled = 0;
336 $record->classname = '\core\task\scheduled_test_task';
337 $DB->insert_record('task_scheduled', $record);
338
339 $record->lastruntime = 1;
340 $record->classname = '\core\task\scheduled_test2_task';
341 $DB->insert_record('task_scheduled', $record);
342
343 // Should get handed the second task.
344 $task = \core\task\manager::get_next_scheduled_task($now);
345 $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
346 $task->execute();
347 \core\task\manager::scheduled_task_complete($task);
348
349 // Should get handed the first task.
350 $task = \core\task\manager::get_next_scheduled_task($now);
351 $this->assertInstanceOf('\core\task\scheduled_test_task', $task);
352 $task->execute();
353 \core\task\manager::scheduled_task_complete($task);
354
355 // Should not get any task.
356 $task = \core\task\manager::get_next_scheduled_task($now);
357 $this->assertNull($task);
309ae892 358 }
a0ac4060
DW
359
360 public function test_get_broken_scheduled_task() {
361 global $DB;
362
363 $this->resetAfterTest(true);
364 // Delete all existing scheduled tasks.
365 $DB->delete_records('task_scheduled');
366 // Add a scheduled task.
367
368 // A broken task that runs all the time.
369 $record = new stdClass();
370 $record->blocking = true;
371 $record->minute = '*';
372 $record->hour = '*';
373 $record->dayofweek = '*';
374 $record->day = '*';
375 $record->month = '*';
376 $record->component = 'test_scheduled_task';
377 $record->classname = '\core\task\scheduled_test_task_broken';
378
379 $DB->insert_record('task_scheduled', $record);
380
381 $now = time();
382 // Should not get any task.
383 $task = \core\task\manager::get_next_scheduled_task($now);
384 $this->assertDebuggingCalled();
385 $this->assertNull($task);
386 }
16078807
DP
387
388 /**
389 * Tests the use of 'R' syntax in time fields of tasks to get
390 * tasks be configured with a non-uniform time.
391 */
392 public function test_random_time_specification() {
393
394 // Testing non-deterministic things in a unit test is not really
395 // wise, so we just test the values have changed within allowed bounds.
396 $testclass = new \core\task\scheduled_test_task();
397
398 // The test task defaults to '*'.
399 $this->assertInternalType('string', $testclass->get_minute());
400 $this->assertInternalType('string', $testclass->get_hour());
401
402 // Set a random value.
403 $testclass->set_minute('R');
404 $testclass->set_hour('R');
f47e4eb4 405 $testclass->set_day_of_week('R');
16078807
DP
406
407 // Verify the minute has changed within allowed bounds.
408 $minute = $testclass->get_minute();
409 $this->assertInternalType('int', $minute);
410 $this->assertGreaterThanOrEqual(0, $minute);
411 $this->assertLessThanOrEqual(59, $minute);
412
413 // Verify the hour has changed within allowed bounds.
414 $hour = $testclass->get_hour();
415 $this->assertInternalType('int', $hour);
416 $this->assertGreaterThanOrEqual(0, $hour);
417 $this->assertLessThanOrEqual(23, $hour);
f47e4eb4
AD
418
419 // Verify the dayofweek has changed within allowed bounds.
420 $dayofweek = $testclass->get_day_of_week();
421 $this->assertInternalType('int', $dayofweek);
422 $this->assertGreaterThanOrEqual(0, $dayofweek);
423 $this->assertLessThanOrEqual(6, $dayofweek);
16078807 424 }
b49964f7
AG
425
426 /**
427 * Test that the file_temp_cleanup_task removes directories and
428 * files as expected.
429 */
430 public function test_file_temp_cleanup_task() {
431 global $CFG;
ef844148 432 $backuptempdir = make_backup_temp_directory('');
b49964f7
AG
433
434 // Create directories.
ef844148 435 $dir = $backuptempdir . DIRECTORY_SEPARATOR . 'backup01' . DIRECTORY_SEPARATOR . 'courses';
b49964f7
AG
436 mkdir($dir, 0777, true);
437
438 // Create files to be checked and then deleted.
439 $file01 = $dir . DIRECTORY_SEPARATOR . 'sections.xml';
440 file_put_contents($file01, 'test data 001');
441 $file02 = $dir . DIRECTORY_SEPARATOR . 'modules.xml';
442 file_put_contents($file02, 'test data 002');
443 // Change the time modified for the first file, to a time that will be deleted by the task (greater than seven days).
444 touch($file01, time() - (8 * 24 * 3600));
445
446 $task = \core\task\manager::get_scheduled_task('\\core\\task\\file_temp_cleanup_task');
447 $this->assertInstanceOf('\core\task\file_temp_cleanup_task', $task);
448 $task->execute();
449
450 // Scan the directory. Only modules.xml should be left.
451 $filesarray = scandir($dir);
452 $this->assertEquals('modules.xml', $filesarray[2]);
453 $this->assertEquals(3, count($filesarray));
454
455 // Change the time modified on modules.xml.
456 touch($file02, time() - (8 * 24 * 3600));
457 // Change the time modified on the courses directory.
ef844148 458 touch($backuptempdir . DIRECTORY_SEPARATOR . 'backup01' . DIRECTORY_SEPARATOR .
b49964f7
AG
459 'courses', time() - (8 * 24 * 3600));
460 // Run the scheduled task to remove the file and directory.
461 $task->execute();
ef844148 462 $filesarray = scandir($backuptempdir . DIRECTORY_SEPARATOR . 'backup01');
b49964f7
AG
463 // There should only be two items in the array, '.' and '..'.
464 $this->assertEquals(2, count($filesarray));
465
466 // Change the time modified on all of the files and directories.
467 $dir = new \RecursiveDirectoryIterator($CFG->tempdir);
468 // Show all child nodes prior to their parent.
469 $iter = new \RecursiveIteratorIterator($dir, \RecursiveIteratorIterator::CHILD_FIRST);
470
471 for ($iter->rewind(); $iter->valid(); $iter->next()) {
d83a1c11
RT
472 if ($iter->isDir() && !$iter->isDot()) {
473 $node = $iter->getRealPath();
474 touch($node, time() - (8 * 24 * 3600));
475 }
b49964f7
AG
476 }
477
478 // Run the scheduled task again to remove all of the files and directories.
479 $task->execute();
480 $filesarray = scandir($CFG->tempdir);
481 // All of the files and directories should be deleted.
ef844148
MS
482 // There should only be three items in the array, '.', '..' and '.htaccess'.
483 $this->assertEquals([ '.', '..', '.htaccess' ], $filesarray);
b49964f7 484 }
470d59d3 485
486 /**
487 * Test that the function to clear the fail delay from a task works correctly.
488 */
489 public function test_clear_fail_delay() {
490
491 $this->resetAfterTest();
492
493 // Get an example task to use for testing. Task is set to run every minute by default.
494 $taskname = '\core\task\send_new_user_passwords_task';
495
496 // Pretend task started running and then failed 3 times.
497 $before = time();
498 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
499 for ($i = 0; $i < 3; $i ++) {
500 $task = \core\task\manager::get_scheduled_task($taskname);
501 $lock = $cronlockfactory->get_lock('\\' . get_class($task), 10);
502 $task->set_lock($lock);
503 \core\task\manager::scheduled_task_failed($task);
504 }
505
506 // Confirm task is now delayed by several minutes.
507 $task = \core\task\manager::get_scheduled_task($taskname);
508 $this->assertEquals(240, $task->get_fail_delay());
509 $this->assertGreaterThan($before + 230, $task->get_next_run_time());
510
511 // Clear the fail delay and re-get the task.
512 \core\task\manager::clear_fail_delay($task);
513 $task = \core\task\manager::get_scheduled_task($taskname);
514
515 // There should be no delay and it should run within the next minute.
516 $this->assertEquals(0, $task->get_fail_delay());
517 $this->assertLessThan($before + 70, $task->get_next_run_time());
518 }
0400b5ef
AN
519
520 /**
521 * Assert that the specified tasks are equal.
522 *
523 * @param \core\task\task_base $task
524 * @param \core\task\task_base $comparisontask
525 */
526 public function assertTaskEquals(\core\task\task_base $task, \core\task\task_base $comparisontask): void {
527 // Convert both to an object.
528 $task = \core\task\manager::record_from_scheduled_task($task);
529 $comparisontask = \core\task\manager::record_from_scheduled_task($comparisontask);
530
531 // Reset the nextruntime field as it is intentionally dynamic.
532 $task->nextruntime = null;
533 $comparisontask->nextruntime = null;
534
535 $args = array_merge(
536 [
537 $task,
538 $comparisontask,
539 ],
540 array_slice(func_get_args(), 2)
541 );
542
543 call_user_func_array([$this, 'assertEquals'], $args);
544 }
545
546 /**
547 * Assert that the specified tasks are not equal.
548 *
549 * @param \core\task\task_base $task
550 * @param \core\task\task_base $comparisontask
551 */
552 public function assertTaskNotEquals(\core\task\task_base $task, \core\task\task_base $comparisontask): void {
553 // Convert both to an object.
554 $task = \core\task\manager::record_from_scheduled_task($task);
555 $comparisontask = \core\task\manager::record_from_scheduled_task($comparisontask);
556
557 // Reset the nextruntime field as it is intentionally dynamic.
558 $task->nextruntime = null;
559 $comparisontask->nextruntime = null;
560
561 $args = array_merge(
562 [
563 $task,
564 $comparisontask,
565 ],
566 array_slice(func_get_args(), 2)
567 );
568
569 call_user_func_array([$this, 'assertNotEquals'], $args);
570 }
309ae892 571}