first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+993
View File
@@ -0,0 +1,993 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\task;
/**
* Test class for adhoc tasks.
*
* @package core
* @category test
* @copyright 2013 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core\task\manager
*/
final class adhoc_task_test extends \advanced_testcase {
public static function setUpBeforeClass(): void {
parent::setUpBeforeClass();
require_once(__DIR__ . '/../fixtures/task_fixtures.php');
}
/**
* Test getting name of task that implements it's own get_name method
*
* @covers \core\task\adhoc_task
*/
public function test_get_name(): void {
$task = new \core\task\adhoc_test_task();
$this->assertEquals('Test adhoc class', $task->get_name());
}
/**
* Test getting name of task that uses the default implementation of get_name
*
* @covers \core\task\adhoc_task
*/
public function test_get_name_default(): void {
$task = new \mod_fake\task\adhoc_component_task();
$this->assertEquals('Adhoc component task', $task->get_name());
}
/**
* Test basic adhoc task execution.
*/
public function test_get_next_adhoc_task_now(): void {
$this->resetAfterTest(true);
// Create an adhoc task.
$task = new adhoc_test_task();
// Queue it.
manager::queue_adhoc_task($task);
$now = time();
// Get it from the scheduler.
$task = manager::get_next_adhoc_task($now);
$this->assertInstanceOf('\\core\\task\\adhoc_test_task', $task);
$task->execute();
manager::adhoc_task_complete($task);
}
/**
* Test basic adhoc task execution.
*/
public function test_get_next_adhoc_task_class(): void {
$this->resetAfterTest(true);
// Create an adhoc task.
$task = new \core\task\adhoc_test_task();
// Queue it.
manager::queue_adhoc_task($task);
$now = time();
$classname = get_class($task);
// The task will not be returned.
$this->assertNull(manager::get_next_adhoc_task($now, true, "{$classname}notexists"));
// Get it from the scheduler.
$task = manager::get_next_adhoc_task($now, true, $classname);
$this->assertInstanceOf('\\core\\task\\adhoc_test_task', $task);
$task->execute();
manager::adhoc_task_complete($task);
}
/**
* Test adhoc task failure retry backoff.
*/
public function test_get_next_adhoc_task_fail_retry(): void {
$this->resetAfterTest(true);
// Create an adhoc task.
$task = new adhoc_test_task();
manager::queue_adhoc_task($task);
$now = time();
// Get it from the scheduler, execute it, and mark it as failed.
$task = manager::get_next_adhoc_task($now);
$taskid = $task->get_id();
$task->execute();
manager::adhoc_task_failed($task);
// The task will not be returned immediately.
$this->assertNull(manager::get_next_adhoc_task($now));
// Should get the adhoc task (retry after delay). Fail it again.
$task = manager::get_next_adhoc_task($now + 120);
$this->assertInstanceOf('\\core\\task\\adhoc_test_task', $task);
$this->assertEquals($taskid, $task->get_id());
$task->execute();
manager::adhoc_task_failed($task);
// Should get the adhoc task immediately.
$task = manager::get_adhoc_task($taskid);
$this->assertInstanceOf('\\core\\task\\adhoc_test_task', $task);
$this->assertEquals($taskid, $task->get_id());
$task->execute();
manager::adhoc_task_complete($task);
// Should not get any task.
$this->assertNull(manager::get_next_adhoc_task($now));
}
/**
* Test that failed tasks eventually hit the maximum delay.
*
* @covers \core\task\adhoc_task
*/
public function test_get_next_adhoc_task_maximum_fail_delay(): void {
$this->resetAfterTest(true);
$now = time();
// Create an adhoc task.
$task = new adhoc_test_task();
$attemptsavailable = $task->get_attempts_available();
manager::queue_adhoc_task($task);
// Exhaust all attempts available.
for ($x = 0; $x < $attemptsavailable; $x++) {
$delay = $task->get_fail_delay() * 2;
$task = manager::get_next_adhoc_task($now + $delay);
$task->execute();
manager::adhoc_task_failed($task);
}
// Check that the fail delay is now set to 24 hours (the maximum amount of times).
$this->assertEquals(DAYSECS, $task->get_fail_delay());
}
/**
* Test adhoc task failure retry backoff.
*/
public function test_adhoc_task_with_retry_flag(): void {
global $DB;
$this->resetAfterTest();
$now = time();
// Create a normal adhoc task.
$task = new adhoc_test_task();
$taskid1 = manager::queue_adhoc_task(task: $task);
// This is a normal task, so it should have limited attempts.
$attemptsavailable = $DB->get_field(
table: 'task_adhoc',
return: 'attemptsavailable',
conditions: ['id' => $taskid1]
);
$this->assertEquals(expected: 12, actual: $attemptsavailable);
// Get the task from the scheduler, execute it, and mark it as failed.
$task = manager::get_next_adhoc_task(timestart: $now);
$taskid1 = $task->get_id();
$task->execute();
manager::adhoc_task_failed(task: $task);
// Now that the task has failed, there should be one less attempt available.
$attemptsavailable = $DB->get_field(
table: 'task_adhoc',
return: 'attemptsavailable',
conditions: ['id' => $taskid1]
);
$this->assertEquals(expected: 12 - 1, actual: $attemptsavailable);
// Create a no-retry adhoc task.
$now = time();
$task = new no_retry_adhoc_task();
$taskid2 = manager::queue_adhoc_task(task: $task);
// This is no-retry task, so it should have only 1 attempt available.
$attemptsavailable = $DB->get_field(
table: 'task_adhoc',
return: 'attemptsavailable',
conditions: ['id' => $taskid2]
);
$this->assertEquals(
expected: 1,
actual: $attemptsavailable,
);
// Get the task from the scheduler, execute it, and mark it as failed.
$task = manager::get_next_adhoc_task(timestart: $now);
$taskid2 = $task->get_id();
$task->execute();
manager::adhoc_task_failed(task: $task);
// This is no-retry task, the remaining available attempts should be reduced to 0.
$attemptsavailable = $DB->get_field(
table: 'task_adhoc',
return: 'attemptsavailable',
conditions: ['id' => $taskid2]
);
$this->assertEquals(
expected: 0,
actual: $attemptsavailable,
);
// There will be two records in the task_adhoc table, one for each task.
$this->assertEquals(
expected: 2,
actual: $DB->count_records(table: 'task_adhoc')
);
// But get_next_adhoc_task() should return only the allowed re-try task.
// The no-retry task should not be returned because it has no remaining attempts.
do {
$task = manager::get_next_adhoc_task(timestart: $now + 86400);
if ($task) {
manager::adhoc_task_failed(task: $task);
$this->assertEquals(
expected: $taskid1,
actual: $task->get_id(),
);
}
} while ($task);
// Mark the normal task as complete.
$task = manager::get_adhoc_task(taskid: $taskid1);
manager::adhoc_task_complete($task);
// There will be one record in the task_adhoc table.
$this->assertEquals(
expected: 1,
actual: $DB->count_records(table: 'task_adhoc')
);
// But get_next_adhoc_task() should return nothing.
$this->assertNull(manager::get_next_adhoc_task(timestart: $now + 86400));
}
/**
* Test adhoc task failure cleanup.
*/
public function test_adhoc_task_clean_up(): void {
global $DB, $CFG;
$this->resetAfterTest();
// Create two no-retry adhoc tasks.
$task1 = new no_retry_adhoc_task();
$taskid1 = manager::queue_adhoc_task(task: $task1);
$task2 = new no_retry_adhoc_task();
$taskid2 = manager::queue_adhoc_task(task: $task2);
// Get the tasks and mark it as failed.
$task = manager::get_adhoc_task($taskid1);
manager::adhoc_task_failed(task: $task);
$task = manager::get_adhoc_task($taskid2);
manager::adhoc_task_failed(task: $task);
// These are no-retry tasks, the remaining available attempts should be reduced to 0.
$this->assertEquals(
expected: 0,
actual: $DB->get_field(
table: 'task_adhoc',
return: 'attemptsavailable',
conditions: ['id' => $taskid1],
),
);
$this->assertEquals(
expected: 0,
actual: $DB->get_field(
table: 'task_adhoc',
return: 'attemptsavailable',
conditions: ['id' => $taskid2],
),
);
// There will be two records in the task_adhoc table.
$this->assertEquals(
expected: 2,
actual: $DB->count_records(table: 'task_adhoc'),
);
// Clean up failed adhoc tasks. This will clean nothing because the tasks are not old enough.
manager::clean_failed_adhoc_tasks();
// There will be two records in the task_adhoc table.
$this->assertEquals(
expected: 2,
actual: $DB->count_records(table: 'task_adhoc'),
);
// Update the time of the task2 to be older more than 2 days.
$DB->set_field(
table: 'task_adhoc',
newfield: 'firststartingtime',
newvalue: time() - (DAYSECS * 2) - 10, // Plus 10 seconds to make sure it is older than 2 days.
conditions: ['id' => $taskid2],
);
// Clean up failed adhoc tasks. This will clean nothing because the tasks are not old enough.
manager::clean_failed_adhoc_tasks();
// There will be two records in the task_adhoc table.
$this->assertEquals(
expected: 2,
actual: $DB->count_records(table: 'task_adhoc'),
);
// Update the time of the task1 to be older than the cleanup time.
$DB->set_field(
table: 'task_adhoc',
newfield: 'firststartingtime',
// Plus 10 seconds to make sure it is older than the retention time.
newvalue: time() - $CFG->task_adhoc_failed_retention - 10,
conditions: ['id' => $taskid1],
);
// Clean up failed adhoc tasks. task1 should be cleaned now.
manager::clean_failed_adhoc_tasks();
// There will be one record in the task_adhoc table.
$this->assertEquals(
expected: 1,
actual: $DB->count_records(table: 'task_adhoc'),
);
// Update the duration of the Failed ad hoc task retention period to one day.
$CFG->task_adhoc_failed_retention = DAYSECS;
// Clean up failed adhoc tasks. task2 should be cleaned now.
manager::clean_failed_adhoc_tasks();
// The task_adhoc table should be empty now.
$this->assertEquals(
expected: 0,
actual: $DB->count_records(table: 'task_adhoc'),
);
}
/**
* Test adhoc task failure will retain the time information.
*/
public function test_adhoc_task_failed_will_retain_time_info(): void {
global $DB;
$this->resetAfterTest();
$now = time();
// Create an adhoc task.
$task = new adhoc_test_task();
// Queue it.
$taskid = manager::queue_adhoc_task(task: $task);
// Update the timecreated of the task to be older.
$DB->set_field(
table: 'task_adhoc',
newfield: 'timecreated',
newvalue: time() - DAYSECS,
conditions: ['id' => $taskid],
);
// Get the timecreated value before marking the task as failed.
$timecreatedbefore = $DB->get_field(
table: 'task_adhoc',
return: 'timecreated',
conditions: ['id' => $taskid],
);
// Get the task from the scheduler.
$task = manager::get_next_adhoc_task(timestart: $now);
// Execute the task.
$task->execute();
// Mark the task as failed.
manager::adhoc_task_failed(task: $task);
// Get the timecreated value after marking the task as failed.
$timecreatedafter = $DB->get_field(
table: 'task_adhoc',
return: 'timecreated',
conditions: ['id' => $taskid],
);
// The timecreated values should be the same.
$this->assertEquals($timecreatedbefore, $timecreatedafter);
}
/**
* Test future adhoc task execution.
*/
public function test_get_next_adhoc_task_future(): void {
$this->resetAfterTest(true);
$now = time();
// Create an adhoc task in future.
$task = new adhoc_test_task();
$task->set_next_run_time($now + 1000);
manager::queue_adhoc_task($task);
// Fetching the next task should not return anything.
$this->assertNull(manager::get_next_adhoc_task($now));
// Fetching in the future should return the task.
$task = manager::get_next_adhoc_task($now + 1020);
$this->assertInstanceOf('\\core\\task\\adhoc_test_task', $task);
$task->execute();
manager::adhoc_task_complete($task);
}
/**
* Test queueing an adhoc task belonging to a component, where we set the task component accordingly
*/
public function test_queue_adhoc_task_for_component(): void {
$this->resetAfterTest();
$task = new \mod_forum\task\send_user_digests();
$task->set_component('mod_test');
manager::queue_adhoc_task($task);
$this->assertDebuggingNotCalled();
}
/**
* Test queueing an adhoc task belonging to a component, where we do not set the task component
*/
public function test_queue_task_for_component_without_set_component(): void {
$this->resetAfterTest();
$task = new \mod_forum\task\send_user_digests();
manager::queue_adhoc_task($task);
$this->assertDebuggingNotCalled();
// Assert the missing component was set.
$this->assertEquals('mod_forum', $task->get_component());
}
/**
* Test queueing an adhoc task belonging to an invalid component, where we do not set the task component
*/
public function test_queue_task_for_invalid_component_without_set_component(): void {
$this->resetAfterTest();
$task = new \mod_fake\task\adhoc_component_task();
manager::queue_adhoc_task($task);
$this->assertDebuggingCalled('Component not set and the class namespace does not match a valid component (mod_fake).');
}
/**
* Test empty set of adhoc tasks
*/
public function test_get_adhoc_tasks_empty_set(): void {
$this->resetAfterTest(true);
$this->assertEquals([], manager::get_adhoc_tasks('\\core\\task\\adhoc_test_task'));
}
/**
* Test correct set of adhoc tasks is returned for class.
*/
public function test_get_adhoc_tasks_result_set(): void {
$this->resetAfterTest(true);
for ($i = 0; $i < 3; $i++) {
$task = new adhoc_test_task();
manager::queue_adhoc_task($task);
}
for ($i = 0; $i < 3; $i++) {
$task = new adhoc_test2_task();
manager::queue_adhoc_task($task);
}
$adhoctests = manager::get_adhoc_tasks('\\core\\task\\adhoc_test_task');
$adhoctest2s = manager::get_adhoc_tasks('\\core\\task\\adhoc_test2_task');
$this->assertCount(3, $adhoctests);
$this->assertCount(3, $adhoctest2s);
foreach ($adhoctests as $task) {
$this->assertInstanceOf('\\core\\task\\adhoc_test_task', $task);
}
foreach ($adhoctest2s as $task) {
$this->assertInstanceOf('\\core\\task\\adhoc_test2_task', $task);
}
}
/**
* Ensure that the reschedule_or_queue_adhoc_task function will schedule a new task if no tasks exist.
*/
public function test_reschedule_or_queue_adhoc_task_no_existing(): void {
$this->resetAfterTest(true);
// Schedule adhoc task.
$task = new adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
manager::reschedule_or_queue_adhoc_task($task);
$this->assertEquals(1, count(manager::get_adhoc_tasks('core\task\adhoc_test_task')));
}
/**
* Ensure that the reschedule_or_queue_adhoc_task function will schedule a new task if a task for the same user does
* not exist.
*/
public function test_reschedule_or_queue_adhoc_task_different_user(): void {
$this->resetAfterTest(true);
$user = \core_user::get_user_by_username('admin');
// Schedule adhoc task.
$task = new adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
manager::reschedule_or_queue_adhoc_task($task);
// Schedule adhoc task for a different user.
$task = new adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
$task->set_userid($user->id);
manager::reschedule_or_queue_adhoc_task($task);
$this->assertEquals(2, count(manager::get_adhoc_tasks('core\task\adhoc_test_task')));
}
/**
* Ensure that the reschedule_or_queue_adhoc_task function will schedule a new task if a task with different custom
* data exists.
*/
public function test_reschedule_or_queue_adhoc_task_different_data(): void {
$this->resetAfterTest(true);
// Schedule adhoc task.
$task = new adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
manager::reschedule_or_queue_adhoc_task($task);
// Schedule adhoc task for a different user.
$task = new adhoc_test_task();
$task->set_custom_data(['courseid' => 11]);
manager::reschedule_or_queue_adhoc_task($task);
$this->assertEquals(2, count(manager::get_adhoc_tasks('core\task\adhoc_test_task')));
}
/**
* Ensure that the reschedule_or_queue_adhoc_task function will not make any change for matching data if no time was
* specified.
*/
public function test_reschedule_or_queue_adhoc_task_match_no_change(): void {
$this->resetAfterTest(true);
// Schedule adhoc task.
$task = new adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
$task->set_next_run_time(time() + DAYSECS);
manager::reschedule_or_queue_adhoc_task($task);
$before = manager::get_adhoc_tasks('core\task\adhoc_test_task');
// Schedule the task again but do not specify a time.
$task = new adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
manager::reschedule_or_queue_adhoc_task($task);
$this->assertEquals(1, count(manager::get_adhoc_tasks('core\task\adhoc_test_task')));
$this->assertEquals($before, manager::get_adhoc_tasks('core\task\adhoc_test_task'));
}
/**
* Ensure that the reschedule_or_queue_adhoc_task function will update the run time if there are planned changes.
*/
public function test_reschedule_or_queue_adhoc_task_match_update_runtime(): void {
$this->resetAfterTest(true);
$initialruntime = time() + DAYSECS;
$newruntime = time() + WEEKSECS;
// Schedule adhoc task.
$task = new adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
$task->set_next_run_time($initialruntime);
manager::reschedule_or_queue_adhoc_task($task);
$before = manager::get_adhoc_tasks('core\task\adhoc_test_task');
// Schedule the task again.
$task = new adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
$task->set_next_run_time($newruntime);
manager::reschedule_or_queue_adhoc_task($task);
$tasks = manager::get_adhoc_tasks('core\task\adhoc_test_task');
$this->assertEquals(1, count($tasks));
$this->assertNotEquals($before, $tasks);
$firsttask = reset($tasks);
$this->assertEquals($newruntime, $firsttask->get_next_run_time());
}
/**
* Test queue_adhoc_task "if not scheduled".
*/
public function test_queue_adhoc_task_if_not_scheduled(): void {
$this->resetAfterTest(true);
$user = \core_user::get_user_by_username('admin');
// Schedule adhoc task.
$task = new adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
$this->assertNotEmpty(manager::queue_adhoc_task($task, true));
$this->assertEquals(1, count(manager::get_adhoc_tasks('core\task\adhoc_test_task')));
// Schedule adhoc task with a user.
$task = new adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
$task->set_userid($user->id);
$this->assertNotEmpty(manager::queue_adhoc_task($task, true));
$this->assertEquals(2, count(manager::get_adhoc_tasks('core\task\adhoc_test_task')));
// Schedule same adhoc task with different custom data.
$task = new adhoc_test_task();
$task->set_custom_data(['courseid' => 1]);
$this->assertNotEmpty(manager::queue_adhoc_task($task, true));
$this->assertEquals(3, count(manager::get_adhoc_tasks('core\task\adhoc_test_task')));
// Schedule same adhoc task with same custom data.
$task = new adhoc_test_task();
$task->set_custom_data(['courseid' => 1]);
$this->assertEmpty(manager::queue_adhoc_task($task, true));
$this->assertEquals(3, count(manager::get_adhoc_tasks('core\task\adhoc_test_task')));
// Schedule same adhoc task with same custom data and a user.
$task = new adhoc_test_task();
$task->set_custom_data(['courseid' => 1]);
$task->set_userid($user->id);
$this->assertNotEmpty(manager::queue_adhoc_task($task, true));
$this->assertEquals(4, count(manager::get_adhoc_tasks('core\task\adhoc_test_task')));
// Schedule same adhoc task without custom data.
// Note: This task was created earlier.
$task = new adhoc_test_task();
$this->assertNotEmpty(manager::queue_adhoc_task($task, true));
$this->assertEquals(5, count(manager::get_adhoc_tasks('core\task\adhoc_test_task')));
// Schedule same adhoc task without custom data (again).
$task5 = new adhoc_test_task();
$this->assertEmpty(manager::queue_adhoc_task($task5, true));
$this->assertEquals(5, count(manager::get_adhoc_tasks('core\task\adhoc_test_task')));
// Schedule same adhoc task without custom data but with a userid.
$task6 = new adhoc_test_task();
$user = \core_user::get_user_by_username('admin');
$task6->set_userid($user->id);
$this->assertNotEmpty(manager::queue_adhoc_task($task6, true));
$this->assertEquals(6, count(manager::get_adhoc_tasks('core\task\adhoc_test_task')));
// Schedule same adhoc task again without custom data but with a userid.
$task6 = new adhoc_test_task();
$user = \core_user::get_user_by_username('admin');
$task6->set_userid($user->id);
$this->assertEmpty(manager::queue_adhoc_task($task6, true));
$this->assertEquals(6, count(manager::get_adhoc_tasks('core\task\adhoc_test_task')));
}
/**
* Test that when no userid is specified, it returns empty from the DB
* too.
* @covers \core\task\adhoc_task
*/
public function test_adhoc_task_user_empty(): void {
$this->resetAfterTest(true);
// Create an adhoc task in future.
$task = new adhoc_test_task();
manager::queue_adhoc_task($task);
// Get it back from the scheduler.
$now = time();
$task = manager::get_next_adhoc_task($now);
manager::adhoc_task_complete($task);
$this->assertEmpty($task->get_userid());
}
/**
* Test that when a userid is specified, that userid is subsequently
* returned.
*
* @covers \core\task\adhoc_task
*/
public function test_adhoc_task_user_set(): void {
$this->resetAfterTest(true);
// Create an adhoc task in future.
$task = new adhoc_test_task();
$user = \core_user::get_user_by_username('admin');
$task->set_userid($user->id);
manager::queue_adhoc_task($task);
// Get it back from the scheduler.
$now = time();
$task = manager::get_next_adhoc_task($now);
manager::adhoc_task_complete($task);
$this->assertEquals($user->id, $task->get_userid());
}
/**
* Test adhoc task with the first starting time.
*/
public function test_adhoc_task_get_first_starting_time(): void {
global $DB;
$this->resetAfterTest(true);
$now = time();
// Create an adhoc task.
$task = new adhoc_test_task();
// Queue it.
$taskid = manager::queue_adhoc_task(task: $task);
// Get the firststartingtime value.
$firststartingtime = $DB->get_field(
table: 'task_adhoc',
return: 'firststartingtime',
conditions: ['id' => $taskid],
);
$this->assertNull($firststartingtime);
// This will make sure that the task will be started after the $now value.
sleep(3);
// Get the task from the scheduler.
$task = manager::get_next_adhoc_task(timestart: $now);
// Mark the task as starting.
manager::adhoc_task_starting($task);
// Execute the task.
$task->execute();
// Mark the task as failed.
manager::adhoc_task_failed(task: $task);
// Get the firststartingtime value.
$origintimestarted = $DB->get_field(
table: 'task_adhoc',
return: 'firststartingtime',
conditions: ['id' => $taskid],
);
$this->assertNotNull($origintimestarted);
$this->assertGreaterThan($now, $origintimestarted);
// Get the task from the scheduler.
$task = manager::get_next_adhoc_task(timestart: $now + 86400);
// Mark the task as starting.
manager::adhoc_task_starting($task);
// Execute the task.
$task->execute();
// Mark the task as failed.
manager::adhoc_task_failed(task: $task);
// Get the firststartingtime value.
$firststartingtime = $DB->get_field(
table: 'task_adhoc',
return: 'firststartingtime',
conditions: ['id' => $taskid],
);
// The firststartingtime value should not be changed.
$this->assertEquals($origintimestarted, $firststartingtime);
}
/**
* Test get_concurrency_limit() method to return 0 by default.
*
* @covers \core\task\adhoc_task
*/
public function test_get_concurrency_limit(): void {
$this->resetAfterTest(true);
$task = new adhoc_test_task();
$concurrencylimit = $task->get_concurrency_limit();
$this->assertEquals(0, $concurrencylimit);
}
/**
* Test get_concurrency_limit() method to return a default value set in config.
* @covers \core\task\adhoc_task
*/
public function test_get_concurrency_limit_default(): void {
$this->resetAfterTest(true);
set_config('task_concurrency_limit_default', 10);
$task = new adhoc_test_task();
$concurrencylimit = $task->get_concurrency_limit();
$this->assertEquals(10, $concurrencylimit);
}
/**
* Test get_concurrency_limit() method to return a value for specific task class.
* @covers \core\task\adhoc_task
*/
public function test_get_concurrency_limit_for_task(): void {
global $CFG;
$this->resetAfterTest(true);
set_config('task_concurrency_limit_default', 10);
$CFG->task_concurrency_limit = ['core\task\adhoc_test_task' => 5];
$task = new adhoc_test_task();
$concurrencylimit = $task->get_concurrency_limit();
$this->assertEquals(5, $concurrencylimit);
}
/**
* Test adhoc task sorting.
*/
public function test_get_next_adhoc_task_sorting(): void {
$this->resetAfterTest(true);
// Create adhoc tasks.
$task1 = new adhoc_test_task();
$task1->set_next_run_time(1510000000);
$task1->set_custom_data_as_string('Task 1');
manager::queue_adhoc_task($task1);
$task2 = new adhoc_test_task();
$task2->set_next_run_time(1520000000);
$task2->set_custom_data_as_string('Task 2');
manager::queue_adhoc_task($task2);
$task3 = new adhoc_test_task();
$task3->set_next_run_time(1520000000);
$task3->set_custom_data_as_string('Task 3');
manager::queue_adhoc_task($task3);
// Shuffle tasks.
$task1->set_next_run_time(1540000000);
manager::reschedule_or_queue_adhoc_task($task1);
$task3->set_next_run_time(1530000000);
manager::reschedule_or_queue_adhoc_task($task3);
$task2->set_next_run_time(1530000000);
manager::reschedule_or_queue_adhoc_task($task2);
// Confirm, that tasks are sorted by nextruntime and then by id (ascending).
$task = manager::get_next_adhoc_task(time());
$this->assertEquals('Task 2', $task->get_custom_data_as_string());
manager::adhoc_task_complete($task);
$task = manager::get_next_adhoc_task(time());
$this->assertEquals('Task 3', $task->get_custom_data_as_string());
manager::adhoc_task_complete($task);
$task = manager::get_next_adhoc_task(time());
$this->assertEquals('Task 1', $task->get_custom_data_as_string());
manager::adhoc_task_complete($task);
}
/**
* Test adhoc task run from CLI.
*/
public function test_run_adhoc_from_cli(): void {
$this->resetAfterTest(true);
$taskid = 1;
if (!manager::is_runnable()) {
$this->markTestSkipped("Cannot run tasks");
}
ob_start();
manager::run_adhoc_from_cli($taskid);
$output = ob_get_contents();
ob_end_clean();
$this->assertMatchesRegularExpression(
sprintf('!admin/cli/adhoc_task.php\W+--id=%d\W+--force!', $taskid),
$output
);
}
/**
* Test adhoc class run from CLI.
*/
public function test_run_all_adhoc_from_cli(): void {
$this->resetAfterTest(true);
$classname = 'fake';
if (!manager::is_runnable()) {
$this->markTestSkipped("Cannot run tasks");
}
ob_start();
manager::run_all_adhoc_from_cli(false, $classname);
$output = ob_get_contents();
ob_end_clean();
$this->assertMatchesRegularExpression(
sprintf('!admin/cli/adhoc_task.php\W+--classname=%s\W+--force!', $classname),
$output
);
}
/**
* Test send messages when adhoc task reaches the max fail delay time.
*
* @covers ::adhoc_task_failed
* @covers ::send_failed_task_max_delay_message
*/
public function test_adhoc_message_max_fail_delay(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Redirect messages.
$messagesink = $this->redirectMessages();
// Create an adhoc task.
$task = new adhoc_test_task();
manager::queue_adhoc_task($task);
$now = time();
// Get it from the scheduler, execute it, and mark it as failed.
$task = manager::get_next_adhoc_task($now);
$taskid = $task->get_id();
$task->execute();
// Catch the message. The task has not reach the max time delay yet.
manager::adhoc_task_failed($task);
$messages = $messagesink->get_messages();
$this->assertCount(0, $messages);
// Should get the adhoc task immediately.
$task = manager::get_adhoc_task($taskid);
$task->set_fail_delay(86400);
$this->assertInstanceOf('\\core\\task\\adhoc_test_task', $task);
$this->assertEquals($taskid, $task->get_id());
$task->execute();
// Catch the message.
manager::adhoc_task_failed($task);
$messages = $messagesink->get_messages();
$this->assertCount(1, $messages);
// Get the task and execute it second time.
$task = manager::get_adhoc_task($taskid);
// Set the fail delay to 12 hours.
$task->set_fail_delay(43200);
$task->execute();
manager::adhoc_task_failed($task);
// Catch the message.
$messages = $messagesink->get_messages();
$this->assertCount(2, $messages);
// Get the task and execute it third time.
$task = manager::get_adhoc_task($taskid);
// Set the fail delay to 48 hours.
$task->set_fail_delay(172800);
$task->execute();
manager::adhoc_task_failed($task);
// Catch the message.
$messages = $messagesink->get_messages();
$this->assertCount(3, $messages);
// Check first message information.
$this->assertStringContainsString('Task failed: Test adhoc class', $messages[0]->subject);
$this->assertEquals('failedtaskmaxdelay', $messages[0]->eventtype);
$this->assertEquals('-10', $messages[0]->useridfrom);
$this->assertEquals('2', $messages[0]->useridto);
// Close sink.
$messagesink->close();
}
}
@@ -0,0 +1,150 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\task;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/backup/util/helper/backup_cron_helper.class.php');
/**
* Class containing unit tests for the task do the automation backup and report.
*
* @package core
* @copyright 2024 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class automated_backup_task_test extends \advanced_testcase {
use task_trait;
/**
* Test the automated backup and report tasks.
*
* @covers \automated_backup_report_task::execute
* @covers \backup_cron_automated_helper::send_backup_status_to_admin
* @covers \backup_cron_automated_helper::run_automated_backup
* @covers \backup_cron_automated_helper::check_and_push_automated_backups
*/
public function test_automated_backup(): void {
global $DB;
$this->resetAfterTest();
// Enable automated back up.
set_config(
'backup_auto_active',
true,
'backup',
);
set_config(
'backup_auto_weekdays',
'1111111',
'backup',
);
// Create courses.
$course1 = $this->getDataGenerator()->create_course();
$course2 = $this->getDataGenerator()->create_course();
// Create course backups.
$DB->insert_records(
'backup_courses',
[
[
'courseid' => $course1->id,
'laststatus' => \backup_cron_automated_helper::BACKUP_STATUS_NOTYETRUN,
'nextstarttime' => time() - 10,
],
[
'courseid' => $course2->id,
'laststatus' => \backup_cron_automated_helper::BACKUP_STATUS_NOTYETRUN,
'nextstarttime' => time() - 10,
],
],
);
// Verify that we don't have any running backup tasks.
$this->assertEmpty(get_config('backup', 'backup_auto_adhoctasks'));
$this->assertEmpty(get_config('backup', 'backup_auto_emailpending'));
// Redirect messages to sink.
$sink = $this->redirectMessages();
// Trigger the automated backup scheduled task.
$this->execute_task('\core\task\automated_backup_task');
$messages = $sink->get_messages();
$sink->close();
// Scheduled task should not send report yet, because there are still running backup tasks.
$this->assertCount(0, $messages);
// Check that the backup tasks have been created.
$this->assertTrue($DB->record_exists('backup_courses', ['courseid' => $course1->id]));
$this->assertTrue($DB->record_exists('backup_courses', ['courseid' => $course2->id]));
$this->assertNotEmpty(get_config('backup', 'backup_auto_adhoctasks'));
$this->assertEquals(1, get_config('backup', 'backup_auto_emailpending'));
// Redirect messages to sink and stop buffer output from CLI task.
$sink = $this->redirectMessages();
// Trigger the automated backup report scheduled task.
$this->execute_task('\core\task\automated_backup_report_task');
$messages = $sink->get_messages();
$sink->close();
// Scheduled task should not send report yet, because there are still running backup tasks.
$this->assertCount(0, $messages);
// Execute only one ad-hoc backup task.
$value = get_config('backup', 'backup_auto_adhoctasks');
$queuedtasks = explode(',', $value);
$task = manager::get_adhoc_task($queuedtasks[0]);
$this->start_output_buffering();
$task->execute();
$this->stop_output_buffering();
manager::adhoc_task_complete($task);
// Redirect messages to sink and stop buffer output from CLI task.
$sink = $this->redirectMessages();
// Trigger the automated backup report scheduled task.
$this->execute_task('\core\task\automated_backup_report_task');
$messages = $sink->get_messages();
$sink->close();
// Scheduled task should not send report yet, because there are still running backup tasks.
$this->assertCount(0, $messages);
// Execute the remaining ad-hoc backup task.
$this->start_output_buffering();
$this->runAdhocTasks('\core\task\course_backup_task');
$this->stop_output_buffering();
// Redirect messages to sink.
$sink = $this->redirectMessages();
// Trigger the automated backup report scheduled task.
$this->execute_task('\core\task\automated_backup_report_task');
$messages = $sink->get_messages();
$sink->close();
// Verify that all the backup tasks have been completed and all the configs have been cleared.
$this->assertEmpty(get_config('backup', 'backup_auto_adhoctasks'));
$this->assertEmpty(get_config('backup', 'backup_auto_emailpending'));
// Verify that the report has been sent.
$this->assertCount(1, $messages);
$message = reset($messages);
$this->assertEquals(get_admin()->id, $message->useridto);
$this->assertEquals('backup', $message->eventtype);
}
}
@@ -0,0 +1,80 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\task;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/calendar/lib.php');
/**
* Class containing unit tests for the calendar cron task.
*
* @package core
* @copyright 2017 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class calendar_cron_task_test extends \advanced_testcase {
/**
* Tests set up
*/
protected function setUp(): void {
$this->resetAfterTest();
}
/**
* Test calendar cron task with a working subscription URL.
*/
public function test_cron_working_url(): void {
// ICal URL from external test repo.
$subscriptionurl = $this->getExternalTestFileUrl('/ical.ics');
$subscription = new \stdClass();
$subscription->eventtype = 'site';
$subscription->name = 'test';
$subscription->url = $subscriptionurl;
$subscription->pollinterval = 86400;
$subscription->lastupdated = 0;
calendar_add_subscription($subscription);
$task = new calendar_cron_task();
ob_start();
$task->execute();
$output = ob_get_clean();
$this->assertStringContainsString('events were imported', $output);
$this->assertStringContainsString('events were skipped', $output);
$this->assertStringContainsString('events were updated', $output);
}
/**
* Test calendar cron task with a broken subscription URL.
*/
public function test_cron_broken_url(): void {
$subscription = new \stdClass();
$subscription->eventtype = 'site';
$subscription->name = 'test';
$subscription->url = 'brokenurl';
$subscription->pollinterval = 86400;
$subscription->lastupdated = 0;
calendar_add_subscription($subscription);
$this->expectOutputRegex('/Error updating calendar subscription: The given iCal URL is invalid/');
$task = new calendar_cron_task();
$task->execute();
}
}
@@ -0,0 +1,117 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\task;
use advanced_testcase;
/**
* Class containing unit tests for the daily completion cron task.
*
* @package core
* @copyright 2020 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class completion_daily_task_test extends advanced_testcase {
/**
* Test calendar cron task with a broken subscription URL.
*/
public function test_completion_daily_cron(): void {
global $DB;
$this->resetAfterTest();
set_config('enablecompletion', 1);
set_config('enrol_plugins_enabled', 'self,manual');
$generator = $this->getDataGenerator();
$now = time();
$lastweek = $now - WEEKSECS;
$yesterday = $now - DAYSECS;
$tomorrow = $now + DAYSECS;
// Course with completion enabled and has already started.
$c1 = $generator->create_course(['enablecompletion' => 1, 'startdate' => $lastweek]);
// Course with completion enabled but hasn't started yet.
$c2 = $generator->create_course(['enablecompletion' => 1, 'startdate' => $tomorrow]);
// Completion not enabled.
$c3 = $generator->create_course();
// Create users.
$t1 = $generator->create_user(['username' => 't1']);
$t2 = $generator->create_user(['username' => 't2']);
$s1 = $generator->create_user(['username' => 's1']);
$s2 = $generator->create_user(['username' => 's2']);
// Enrol s1 by self and manual methods to c1.
$generator->enrol_user($s1->id, $c1->id, 'student', 'self', $lastweek);
$generator->enrol_user($s1->id, $c1->id, 'student', 'manual', $yesterday);
// Enrol s1 by self and manual methods to c2.
$generator->enrol_user($s1->id, $c2->id, 'student', 'self');
$generator->enrol_user($s1->id, $c2->id, 'student', 'manual', $tomorrow);
// Enrol s1 by self and manual methods to c3.
$generator->enrol_user($s1->id, $c3->id, 'student', 'self', $lastweek);
$generator->enrol_user($s1->id, $c3->id, 'student');
// Enrol the rest.
foreach ([$c1, $c2, $c3] as $course) {
// Enrol s2 by manual and self enrol methods.
$generator->enrol_user($s2->id, $course->id, 'student', 'self');
$generator->enrol_user($s2->id, $course->id, 'student', 'manual', $course->startdate);
// Enrol t1 as teacher to these courses.
$generator->enrol_user($t1->id, $course->id, 'editingteacher', 'manual', $course->startdate);
$generator->enrol_user($t1->id, $course->id, 'editingteacher', 'manual', $course->startdate);
// Enrol t2 as a non-editing teacher to these courses.
$generator->enrol_user($t1->id, $course->id, 'teacher', 'manual', $course->startdate);
$generator->enrol_user($t1->id, $course->id, 'teacher', 'manual', $course->startdate);
}
// The course completion table should be empty prior to running the task.
$this->assertEquals(0, $DB->count_records('course_completions'));
// Run the daily completion task.
ob_start();
$task = new completion_daily_task();
$task->execute();
ob_end_clean();
// Confirm there are no completion records for teachers nor for courses that haven't started yet or without completion.
list($tsql, $tparams) = $DB->get_in_or_equal([$t1->id, $t2->id], SQL_PARAMS_NAMED);
list($csql, $cparams) = $DB->get_in_or_equal([$c2->id, $c3->id], SQL_PARAMS_NAMED);
$select = "userid $tsql OR course $csql";
$params = array_merge($tparams, $cparams);
$this->assertEmpty($DB->get_records_select('course_completions', $select, $params));
// We should have 2 completion records for both s1 and s2 in course c1.
$this->assertCount(2, $DB->get_records('course_completions'));
// Get s1's completion record from c1.
$s1c1 = $DB->get_record('course_completions', ['userid' => $s1->id, 'course' => $c1->id]);
$this->assertGreaterThanOrEqual($now, $s1c1->timeenrolled);
$this->assertLessThanOrEqual(time(), $s1c1->timeenrolled);
// Get s2's completion record from c1.
$s2c1 = $DB->get_record('course_completions', ['userid' => $s2->id, 'course' => $c1->id]);
$this->assertGreaterThanOrEqual($now, $s2c1->timeenrolled);
$this->assertLessThanOrEqual(time(), $s2c1->timeenrolled);
}
}
+496
View File
@@ -0,0 +1,496 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\task;
/**
* This file contains the unit tests for the database task logger.
*
* @package core
* @category test
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class database_logger_test extends \advanced_testcase {
/**
* @var \moodle_database The original database prior to mocking
*/
protected $DB;
/**
* Setup to backup the database before mocking.
*/
public function setUp(): void {
global $DB;
$this->DB = $DB;
}
/**
* Tear down to unmock the database where it was mocked.
*/
public function tearDown(): void {
global $DB;
$DB = $this->DB;
$this->DB = null;
}
/**
* Ensure that store_log_for_task works with a passing scheduled task.
*/
public function test_store_log_for_task_scheduled(): void {
global $DB;
$this->resetAfterTest();
$endtime = microtime(true);
$starttime = $endtime - 4;
$logdir = make_request_directory();
$logpath = "{$logdir}/log.txt";
file_put_contents($logpath, 'Example content');
$task = new \core\task\cache_cron_task();
database_logger::store_log_for_task($task, $logpath, false, 1, 2, $starttime, $endtime);
$logs = $DB->get_records('task_log');
$this->assertCount(1, $logs);
$log = reset($logs);
$this->assertEquals(file_get_contents($logpath), $log->output);
$this->assertEquals(0, $log->result);
$this->assertEquals(database_logger::TYPE_SCHEDULED, $log->type);
$this->assertEquals('core\task\cache_cron_task', $log->classname);
$this->assertEquals(0, $log->userid);
}
/**
* Ensure that store_log_for_task works with a passing adhoc task.
*/
public function test_store_log_for_task_adhoc(): void {
global $DB;
$this->resetAfterTest();
$endtime = microtime(true);
$starttime = $endtime - 4;
$logdir = make_request_directory();
$logpath = "{$logdir}/log.txt";
file_put_contents($logpath, 'Example content');
$task = $this->getMockBuilder(\core\task\adhoc_task::class)
->onlyMethods(['get_component', 'execute'])
->getMock();
$task->method('get_component')->willReturn('core_test');
database_logger::store_log_for_task($task, $logpath, false, 1, 2, $starttime, $endtime);
$logs = $DB->get_records('task_log');
$this->assertCount(1, $logs);
$log = reset($logs);
$this->assertEquals(file_get_contents($logpath), $log->output);
$this->assertEquals(0, $log->result);
$this->assertEquals(database_logger::TYPE_ADHOC, $log->type);
}
/**
* Ensure that store_log_for_task works with a failing scheduled task.
*/
public function test_store_log_for_task_failed_scheduled(): void {
global $DB;
$this->resetAfterTest();
$endtime = microtime(true);
$starttime = $endtime - 4;
$logdir = make_request_directory();
$logpath = "{$logdir}/log.txt";
file_put_contents($logpath, 'Example content');
$task = new \core\task\cache_cron_task();
database_logger::store_log_for_task($task, $logpath, true, 1, 2, $starttime, $endtime);
$logs = $DB->get_records('task_log');
$this->assertCount(1, $logs);
$log = reset($logs);
$this->assertEquals(file_get_contents($logpath), $log->output);
$this->assertEquals(1, $log->result);
$this->assertEquals(database_logger::TYPE_SCHEDULED, $log->type);
$this->assertEquals('core\task\cache_cron_task', $log->classname);
$this->assertEquals(0, $log->userid);
}
/**
* Ensure that store_log_for_task works with a failing adhoc task.
*/
public function test_store_log_for_task_failed_adhoc(): void {
global $DB;
$this->resetAfterTest();
$endtime = microtime(true);
$starttime = $endtime - 4;
$logdir = make_request_directory();
$logpath = "{$logdir}/log.txt";
file_put_contents($logpath, 'Example content');
$task = $this->getMockBuilder(\core\task\adhoc_task::class)
->onlyMethods(['get_component', 'execute'])
->getMock();
$task->method('get_component')->willReturn('core_test');
database_logger::store_log_for_task($task, $logpath, true, 1, 2, $starttime, $endtime);
$logs = $DB->get_records('task_log');
$this->assertCount(1, $logs);
$log = reset($logs);
$this->assertEquals(file_get_contents($logpath), $log->output);
$this->assertEquals(1, $log->result);
$this->assertEquals(database_logger::TYPE_ADHOC, $log->type);
$this->assertEquals(0, $log->userid);
}
/**
* Ensure that store_log_for_task works with a passing adhoc task run as a specific user.
*/
public function test_store_log_for_task_adhoc_userid(): void {
global $DB;
$this->resetAfterTest();
$endtime = microtime(true);
$starttime = $endtime - 4;
$logdir = make_request_directory();
$logpath = "{$logdir}/log.txt";
file_put_contents($logpath, 'Example content');
$task = $this->getMockBuilder(\core\task\adhoc_task::class)
->onlyMethods(['get_component', 'execute', 'get_userid'])
->getMock();
$task->method('get_component')->willReturn('core_test');
$task->method('get_userid')->willReturn(99);
database_logger::store_log_for_task($task, $logpath, false, 1, 2, $starttime, $endtime);
$logs = $DB->get_records('task_log');
$this->assertCount(1, $logs);
$log = reset($logs);
$this->assertEquals(file_get_contents($logpath), $log->output);
$this->assertEquals(0, $log->result);
$this->assertEquals(database_logger::TYPE_ADHOC, $log->type);
$this->assertEquals(99, $log->userid);
}
/**
* Ensure that the delete_task_logs function performs necessary deletion tasks.
*
* @dataProvider delete_task_logs_provider
* @param mixed $ids
*/
public function test_delete_task_logs($ids): void {
$DB = $this->mock_database();
$DB->expects($this->once())
->method('delete_records_list')
->with(
$this->equalTo('task_log'),
$this->equalTo('id'),
$this->callback(function($deletedids) use ($ids) {
sort($ids);
$idvalues = array_values($deletedids);
sort($idvalues);
return $ids == $idvalues;
})
);
database_logger::delete_task_logs($ids);
}
/**
* Data provider for delete_task_logs tests.
*
* @return array
*/
public function delete_task_logs_provider(): array {
return [
[
[0],
[1],
[1, 2, 3, 4, 5],
],
];
}
/**
* Ensure that the retention period applies correctly.
*/
public function test_cleanup_retention(): void {
global $DB;
$this->resetAfterTest();
// Set a high value for task_logretainruns so that it does no interfere.
set_config('task_logretainruns', 1000);
// Create sample log data - 1 run per hour for 3 days - round down to the start of the hour to avoid time race conditions.
$date = new \DateTime();
$date->setTime($date->format('G'), 0);
$baselogtime = $date->getTimestamp();
for ($i = 0; $i < 3 * 24; $i++) {
$task = new \core\task\cache_cron_task();
$logpath = __FILE__;
database_logger::store_log_for_task($task, $logpath, false, 1, 2, $date->getTimestamp(), $date->getTimestamp() + MINSECS);
$date->sub(new \DateInterval('PT1H'));
}
// Initially there should be 72 runs.
$this->assertCount(72, $DB->get_records('task_log'));
// Note: We set the retention time to a period like DAYSECS minus an adjustment.
// The adjustment is to account for the time taken during setup.
// With a retention period of 2 * DAYSECS, there should only be 47-48 left.
set_config('task_logretention', (2 * DAYSECS) - (time() - $baselogtime));
\core\task\database_logger::cleanup();
$this->assertGreaterThanOrEqual(47, $DB->count_records('task_log'));
$this->assertLessThanOrEqual(48, $DB->count_records('task_log'));
// The oldest should be no more than 48 hours old.
$oldest = $DB->get_records('task_log', [], 'timestart DESC', 'timestart', 0, 1);
$oldest = reset($oldest);
$this->assertGreaterThan(time() - (48 * DAYSECS), $oldest->timestart);
// With a retention period of DAYSECS, there should only be 23 left.
set_config('task_logretention', DAYSECS - (time() - $baselogtime));
\core\task\database_logger::cleanup();
$this->assertGreaterThanOrEqual(23, $DB->count_records('task_log'));
$this->assertLessThanOrEqual(24, $DB->count_records('task_log'));
// The oldest should be no more than 24 hours old.
$oldest = $DB->get_records('task_log', [], 'timestart DESC', 'timestart', 0, 1);
$oldest = reset($oldest);
$this->assertGreaterThan(time() - (24 * DAYSECS), $oldest->timestart);
// With a retention period of 0.5 DAYSECS, there should only be 11 left.
set_config('task_logretention', (DAYSECS / 2) - (time() - $baselogtime));
\core\task\database_logger::cleanup();
$this->assertGreaterThanOrEqual(11, $DB->count_records('task_log'));
$this->assertLessThanOrEqual(12, $DB->count_records('task_log'));
// The oldest should be no more than 12 hours old.
$oldest = $DB->get_records('task_log', [], 'timestart DESC', 'timestart', 0, 1);
$oldest = reset($oldest);
$this->assertGreaterThan(time() - (12 * DAYSECS), $oldest->timestart);
}
/**
* Ensure that the run-count retention applies.
*/
public function test_cleanup_retainruns(): void {
global $DB;
$this->resetAfterTest();
// Set a high value for task_logretention so that it does not interfere.
set_config('task_logretention', YEARSECS);
// Create sample log data - 2 tasks, once per hour for 3 days.
$date = new \DateTime();
$date->setTime($date->format('G'), 0);
$firstdate = $date->getTimestamp();
for ($i = 0; $i < 3 * 24; $i++) {
$task = new \core\task\cache_cron_task();
$logpath = __FILE__;
database_logger::store_log_for_task($task, $logpath, false, 1, 2, $date->getTimestamp(), $date->getTimestamp() + MINSECS);
$task = new \core\task\badges_cron_task();
$logpath = __FILE__;
database_logger::store_log_for_task($task, $logpath, false, 1, 2, $date->getTimestamp(), $date->getTimestamp() + MINSECS);
$date->sub(new \DateInterval('PT1H'));
}
$lastdate = $date->getTimestamp();
// Initially there should be 144 runs - 72 for each task.
$this->assertEquals(144, $DB->count_records('task_log'));
$this->assertEquals(72, $DB->count_records('task_log', ['classname' => \core\task\cache_cron_task::class]));
$this->assertEquals(72, $DB->count_records('task_log', ['classname' => \core\task\badges_cron_task::class]));
// Grab the records for comparison.
$cachecronrecords = array_values($DB->get_records('task_log', ['classname' => \core\task\cache_cron_task::class], 'timestart DESC'));
$badgescronrecords = array_values($DB->get_records('task_log', ['classname' => \core\task\badges_cron_task::class], 'timestart DESC'));
// Configured to retain 144 should have no effect.
set_config('task_logretainruns', 144);
\core\task\database_logger::cleanup();
$this->assertEquals(144, $DB->count_records('task_log'));
$this->assertEquals(72, $DB->count_records('task_log', ['classname' => \core\task\cache_cron_task::class]));
$this->assertEquals(72, $DB->count_records('task_log', ['classname' => \core\task\badges_cron_task::class]));
// The list of records should be identical.
$this->assertEquals($cachecronrecords, array_values($DB->get_records('task_log', ['classname' => \core\task\cache_cron_task::class], 'timestart DESC')));
$this->assertEquals($badgescronrecords, array_values($DB->get_records('task_log', ['classname' => \core\task\badges_cron_task::class], 'timestart DESC')));
// Configured to retain 72 should have no effect either.
set_config('task_logretainruns', 72);
\core\task\database_logger::cleanup();
$this->assertEquals(144, $DB->count_records('task_log'));
$this->assertEquals(72, $DB->count_records('task_log', ['classname' => \core\task\cache_cron_task::class]));
$this->assertEquals(72, $DB->count_records('task_log', ['classname' => \core\task\badges_cron_task::class]));
// The list of records should now only contain the first 72 of each.
$this->assertEquals(
array_slice($cachecronrecords, 0, 72),
array_values($DB->get_records('task_log', ['classname' => \core\task\cache_cron_task::class], 'timestart DESC'))
);
$this->assertEquals(
array_slice($badgescronrecords, 0, 72),
array_values($DB->get_records('task_log', ['classname' => \core\task\badges_cron_task::class], 'timestart DESC'))
);
// Configured to only retain 24 should bring that down to a total of 48, or 24 each.
set_config('task_logretainruns', 24);
\core\task\database_logger::cleanup();
$this->assertEquals(48, $DB->count_records('task_log'));
$this->assertEquals(24, $DB->count_records('task_log', ['classname' => \core\task\cache_cron_task::class]));
$this->assertEquals(24, $DB->count_records('task_log', ['classname' => \core\task\badges_cron_task::class]));
// The list of records should now only contain the first 24 of each.
$this->assertEquals(
array_slice($cachecronrecords, 0, 24),
array_values($DB->get_records('task_log', ['classname' => \core\task\cache_cron_task::class], 'timestart DESC'))
);
$this->assertEquals(
array_slice($badgescronrecords, 0, 24),
array_values($DB->get_records('task_log', ['classname' => \core\task\badges_cron_task::class], 'timestart DESC'))
);
// Configured to only retain 5 should bring that down to a total of 10, or 5 each.
set_config('task_logretainruns', 5);
\core\task\database_logger::cleanup();
$this->assertEquals(10, $DB->count_records('task_log'));
$this->assertEquals(5, $DB->count_records('task_log', ['classname' => \core\task\cache_cron_task::class]));
$this->assertEquals(5, $DB->count_records('task_log', ['classname' => \core\task\badges_cron_task::class]));
// The list of records should now only contain the first 5 of each.
$this->assertEquals(
array_slice($cachecronrecords, 0, 5),
array_values($DB->get_records('task_log', ['classname' => \core\task\cache_cron_task::class], 'timestart DESC'))
);
$this->assertEquals(
array_slice($badgescronrecords, 0, 5),
array_values($DB->get_records('task_log', ['classname' => \core\task\badges_cron_task::class], 'timestart DESC'))
);
// Configured to only retain 0 should bring that down to none.
set_config('task_logretainruns', 0);
\core\task\database_logger::cleanup();
$this->assertEquals(0, $DB->count_records('task_log'));
}
/**
* Ensure that the retention period applies correctly when combined with the run count retention.
*/
public function test_cleanup_combined(): void {
global $DB;
$this->resetAfterTest();
// Calculate date to be used for logs, starting from current time rounded down to nearest hour.
$date = new \DateTime();
$date->setTime($date->format('G'), 0);
$baselogtime = $date->getTimestamp();
// Create sample log data - 2 tasks, once per hour for 3 days.
for ($i = 0; $i < 3 * 24; $i++) {
$task = new \core\task\cache_cron_task();
$logpath = __FILE__;
database_logger::store_log_for_task($task, $logpath, false, 1, 2, $date->getTimestamp(), $date->getTimestamp() + MINSECS);
$task = new \core\task\badges_cron_task();
$logpath = __FILE__;
database_logger::store_log_for_task($task, $logpath, false, 1, 2, $date->getTimestamp(), $date->getTimestamp() + MINSECS);
$date->sub(new \DateInterval('PT1H'));
}
// Initially there should be 144 runs - 72 for each task.
$this->assertEquals(144, $DB->count_records('task_log'));
$this->assertEquals(72, $DB->count_records('task_log', ['classname' => \core\task\cache_cron_task::class]));
$this->assertEquals(72, $DB->count_records('task_log', ['classname' => \core\task\badges_cron_task::class]));
// Note: We set the retention time to a period like DAYSECS minus an adjustment.
// The adjustment is to account for the difference between current time and baselogtime.
// With a retention period of 2 * DAYSECS, there should only be 96 left.
// The run count is a higher number so it will have no effect.
set_config('task_logretention', time() - ($baselogtime - (2 * DAYSECS)) - 1);
set_config('task_logretainruns', 50);
\core\task\database_logger::cleanup();
$this->assertEquals(96, $DB->count_records('task_log'));
$this->assertEquals(48, $DB->count_records('task_log', ['classname' => \core\task\cache_cron_task::class]));
$this->assertEquals(48, $DB->count_records('task_log', ['classname' => \core\task\badges_cron_task::class]));
// We should retain the most recent 48 of each task, so the oldest will be 47 hours old.
$oldest = $DB->get_records('task_log', [], 'timestart ASC', 'timestart', 0, 1);
$oldest = reset($oldest);
$this->assertEquals($baselogtime - (47 * HOURSECS), $oldest->timestart);
// Reducing the retain runs count to 10 should reduce the total logs to 20, overriding the time constraint.
set_config('task_logretainruns', 10);
\core\task\database_logger::cleanup();
$this->assertEquals(20, $DB->count_records('task_log'));
$this->assertEquals(10, $DB->count_records('task_log', ['classname' => \core\task\cache_cron_task::class]));
$this->assertEquals(10, $DB->count_records('task_log', ['classname' => \core\task\badges_cron_task::class]));
// We should retain the most recent 10 of each task, so the oldest will be 9 hours old.
$oldest = $DB->get_records('task_log', [], 'timestart ASC', 'timestart', 0, 1);
$oldest = reset($oldest);
$this->assertEquals($baselogtime - (9 * HOURSECS), $oldest->timestart);
}
/**
* Mock the database.
*/
protected function mock_database() {
global $DB;
$DB = $this->getMockBuilder(\moodle_database::class)
->getMock();
$DB->method('get_record')
->willReturn((object) []);
return $DB;
}
}
@@ -0,0 +1,184 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\task;
/**
* Unit tests for the file_temp_cleanup task.
*
* @package core
* @category test
* @copyright 2013 Tim Gusak <tim.gusak@remote-learner.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core\task\file_temp_cleanup_task
*/
class file_temp_cleanup_task_test extends \basic_testcase {
/**
* Data provider for cron_delete_from_temp.
*
* @return array Provider data
*/
public function cron_delete_from_temp_provider() {
global $CFG;
$tmpdir = realpath($CFG->tempdir);
// This is a relative time.
$time = 0;
// Relative time stamps. Did you know data providers get executed during phpunit init?
$lastweekstime = -($CFG->tempdatafoldercleanup * 3600); // This must match file_temp_cleanup_task.
$beforelastweekstime = $lastweekstime - 3600 - 1; // At least 1h and 1s diff (make it DST immune).
$afterlastweekstime = $lastweekstime + 3600 + 1; // At least 1h and 1s diff (make it DST immune).
$nodes = array();
// Really old directory to remove.
$nodes[] = $this->generate_test_path('/dir1/dir1_1/dir1_1_1/dir1_1_1_1/', true, $lastweekstime * 52, false);
// New Directory to keep.
$nodes[] = $this->generate_test_path('/dir1/dir1_2/', true, $time, true);
// Directory a little less than 1 week old, keep.
$nodes[] = $this->generate_test_path('/dir2/', true, $afterlastweekstime, true);
// Directory older than 1 week old, remove.
$nodes[] = $this->generate_test_path('/dir3/', true, $beforelastweekstime, false);
// File older than 1 week old, remove.
$nodes[] = $this->generate_test_path('/dir1/dir1_1/dir1_1_1/file1_1_1_1', false, $beforelastweekstime, false);
// New File to keep.
$nodes[] = $this->generate_test_path('/dir1/dir1_1/dir1_1_1/file1_1_1_2', false, $time, true);
// File older than 1 week old, remove.
$nodes[] = $this->generate_test_path('/dir1/dir1_2/file1_1_2_1', false, $beforelastweekstime, false);
// New file to keep.
$nodes[] = $this->generate_test_path('/dir1/dir1_2/file1_1_2_2', false, $time, true);
// New file to keep.
$nodes[] = $this->generate_test_path('/file1', false, $time, true);
// File older than 1 week, keep.
$nodes[] = $this->generate_test_path('/file2', false, $beforelastweekstime, false);
// Directory older than 1 week to keep.
// Note: Since this directory contains a directory that contains a file that is also older than a week
// the directory won't be deleted since it's mtime will be updated when the file is deleted.
$nodes[] = $this->generate_test_path('/dir4/dir4_1', true, $beforelastweekstime, true);
$nodes[] = $this->generate_test_path('/dir4/dir4_1/dir4_1_1/', true, $beforelastweekstime, true);
// File older than 1 week to remove.
$nodes[] = $this->generate_test_path('/dir4/dir4_1/dir4_1_1/file4_1_1_1', false, $beforelastweekstime, false);
$expectednodes = array();
foreach ($nodes as $node) {
if ($node->keep) {
$path = $tmpdir;
$pelements = preg_split('/\//', $node->path);
foreach ($pelements as $pelement) {
if ($pelement === '') {
continue;
}
$path .= DIRECTORY_SEPARATOR . $pelement;
if (!in_array($path, $expectednodes)) {
$expectednodes[] = $path;
}
}
}
}
sort($expectednodes);
$data = array(
array(
$nodes,
$expectednodes
),
array(
array(),
array()
)
);
return $data;
}
/**
* Function to populate node array.
*
* @param string $path Path of directory or file
* @param bool $isdir Is the node a directory
* @param int $time modified time of the node in epoch
* @param bool $keep Should the node exist after the delete function has run
*/
private function generate_test_path($path, $isdir = false, $time = 0, $keep = false) {
$node = new \stdClass();
$node->path = $path;
$node->isdir = $isdir;
$node->time = $time;
$node->keep = $keep;
return $node;
}
/**
* Test removing files and directories from tempdir.
*
* @dataProvider cron_delete_from_temp_provider
* @param array $nodes List of files and directories
* @param array $expected The expected results
* @covers ::execute
*/
public function test_cron_delete_from_temp($nodes, $expected): void {
global $CFG;
$tmpdir = realpath($CFG->tempdir);
foreach ($nodes as $data) {
if ($data->isdir) {
mkdir($tmpdir.$data->path, $CFG->directorypermissions, true);
}
}
// We need to iterate through again since adding a file to a directory will
// update the modified time of the directory.
foreach ($nodes as $data) {
touch($tmpdir.$data->path, time() + $data->time);
}
$task = new \core\task\file_temp_cleanup_task();
$task->execute();
$dir = new \RecursiveDirectoryIterator($tmpdir);
$iter = new \RecursiveIteratorIterator($dir, \RecursiveIteratorIterator::CHILD_FIRST);
$actual = array();
for ($iter->rewind(); $iter->valid(); $iter->next()) {
$isvalid = true;
$isvalid = $isvalid && !$iter->isDot();
// Remove the default $CFG->tempdir/backup directory and $CFG->tempdir/.htaccess file from this comparison.
$isvalid = $isvalid && !($iter->isDir() && ($iter->getRealpath() === $tmpdir . DIRECTORY_SEPARATOR . 'backup'));
$isvalid = $isvalid && !($iter->isFile() && ($iter->getRealpath() === $tmpdir . DIRECTORY_SEPARATOR . '.htaccess'));
if ($isvalid) {
$actual[] = $iter->getRealPath();
}
}
// Sort results to guarantee actual order.
sort($actual);
$this->assertEquals($expected, $actual);
}
}
@@ -0,0 +1,78 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\task;
use core_h5p\local\library\autoloader;
use core_h5p\h5p_test_factory;
/**
* Class containing unit tests for the task that fetch the latest version of H5P content types.
*
* @package core
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
* @runTestsInSeparateProcesses
*/
class h5p_get_content_types_task_test extends \advanced_testcase {
protected function setup(): void {
global $CFG;
parent::setUp();
autoloader::register();
require_once($CFG->libdir . '/tests/fixtures/testable_core_h5p.php');
}
/**
* Test task execution
*
* return void
*/
public function test_task_execution(): void {
if (!PHPUNIT_LONGTEST) {
$this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
}
$this->resetAfterTest();
// Fetch generator.
$generator = \testing_util::get_data_generator();
$h5pgenerator = $generator->get_plugin_generator('core_h5p');
$factory = new h5p_test_factory();
$core = $factory->get_core();
$core->set_endpoint($this->getExternalTestFileUrl(''));
$contenttypespending = ['H5P.Accordion'];
$h5pgenerator->create_content_types( $contenttypespending, $core);
// Mock implementation of \core\task\h5p_get_content_types_task::get_core to avoid external systems.
$mocktask = $this->getMockBuilder(\core\task\h5p_get_content_types_task::class)
->onlyMethods(['get_core'])
->getMock();
$mocktask->expects($this->any())
->method('get_core')
->willReturn($core);
$mocktask->execute();
$this->expectOutputRegex('/1 new content types/');
}
}
@@ -0,0 +1,119 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\task;
defined('MOODLE_INTERNAL') || die;
require_once(__DIR__ . '/show_started_courses_task_test.php');
/**
* Class containing unit tests for the hide ended courses task.
*
* It automatically sets the course visibility to hidden when the course end date matches the current day.
*
* @package core
* @copyright 2023 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core\task\hide_ended_courses_task
*/
class hide_ended_courses_task_test extends \core\task\show_started_courses_task_test {
/**
* Test hide_ended_courses cron task.
*
* @dataProvider get_courses_provider
* @covers ::execute
*
* @param int $nextweekvisible Number of courses with the end date set to next week to be created.
* @param int $yesterdayvisible Number of courses with the end date set to yesterday to be created.
* @param int $tomorrowvisible Number of courses with the end date set to tomorrow to be created.
* @param bool $createhidden Whether hidden courses should be created or not.
*/
public function test_hide_ended_courses(
int $nextweekvisible,
int $yesterdayvisible,
int $tomorrowvisible,
bool $createhidden = true
): void {
global $DB;
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$visiblecourses = [];
$hiddencourses = [];
$now = time();
$nextweek = $now + WEEKSECS;
$yesterday = $now - DAYSECS + MINSECS;
$tomorrow = $now + DAYSECS;
// Visible course that finishes last week.
for ($i = 0; $i < $nextweekvisible; $i++) {
$generator->create_course(['visible' => true, 'enddate' => $nextweek]);
}
// Visible course that finished yesterday.
for ($i = 0; $i < $yesterdayvisible; $i++) {
$visiblecourses[] = $generator->create_course(
['visible' => true, 'startdate' => $yesterday - MINSECS , 'enddate' => $yesterday]
)->id;
}
// Visible course that hasn't finished yet.
for ($i = 0; $i < $tomorrowvisible; $i++) {
$generator->create_course(['visible' => true, 'enddate' => $tomorrow]);
}
if ($createhidden) {
// Visible course that already finished.
$hiddencourses[] = $generator->create_course(
['visible' => false, 'startdate' => $yesterday - MINSECS, 'enddate' => $yesterday]
)->id;
// Visible course that hasn't finished yet.
$hiddencourses[] = $generator->create_course(['visible' => false, 'enddate' => $tomorrow])->id;
}
$hiddentotal = count($hiddencourses);
// Course total also includes site course.
$coursetotal = $hiddentotal + $nextweekvisible + $yesterdayvisible + $tomorrowvisible + 1;
// Check current courses have been created correctly.
$this->assertEquals($coursetotal, $DB->count_records('course'));
$this->assertEquals(count($hiddencourses), $DB->count_records('course', ['visible' => 0]));
$sink = $this->redirectEvents();
// Run the hide ended courses task.
ob_start();
$task = new hide_ended_courses_task();
$task->execute();
ob_end_clean();
// Confirm the courses with yesterday as ending date are hidden too. The rest should remain visible.
$courses = $DB->get_records('course', ['visible' => 0], '', 'id');
$this->assertCount($hiddentotal + $yesterdayvisible, $courses);
$expected = array_merge($hiddencourses, $visiblecourses);
$this->assertEquals(asort($expected), asort($courses));
// Check the ended course event has been raised.
$events = $sink->get_events();
$sink->close();
$this->assertCount($yesterdayvisible, $events);
foreach ($events as $event) {
$this->assertInstanceOf('\\core\\event\\course_ended', $event);
$this->assertArrayHasKey($event->courseid, array_flip($expected));
}
}
}
+586
View File
@@ -0,0 +1,586 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file contains the unit tests for the task logging system.
*
* @package core
* @category test
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\task;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../fixtures/task_fixtures.php');
/**
* This file contains the unit tests for the task logging system.
*
* @package core
* @category test
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class logging_test extends \advanced_testcase {
/**
* @var \moodle_database The original database prior to mocking
*/
protected $DB;
/**
* Relevant tearDown for logging tests.
*/
public function tearDown(): void {
global $DB;
// Ensure that any logging is always ended.
logmanager::finalise_log();
if (null !== $this->DB) {
$DB = $this->DB;
$this->DB = null;
}
}
/**
* When the logmode is set to none, logging should not start.
*/
public function test_logmode_none(): void {
global $CFG;
$this->resetAfterTest();
$CFG->task_logmode = logmanager::MODE_NONE;
$initialbufferstate = ob_get_status();
$task = $this->get_test_adhoc_task();
logmanager::start_logging($task);
// There will be no additional output buffer.
$this->assertEquals($initialbufferstate, ob_get_status());
}
/**
* When the logmode is set to all that log capture is started.
*/
public function test_start_logmode_all(): void {
global $CFG;
$this->resetAfterTest();
$CFG->task_logmode = logmanager::MODE_ALL;
$initialbufferstate = ob_get_status();
$task = $this->get_test_adhoc_task();
logmanager::start_logging($task);
// Fetch the new output buffer state.
$state = ob_get_status();
// There will be no additional output buffer.
$this->assertNotEquals($initialbufferstate, $state);
}
/**
* When the logmode is set to fail that log capture is started.
*/
public function test_start_logmode_fail(): void {
global $CFG;
$this->resetAfterTest();
$CFG->task_logmode = logmanager::MODE_FAILONLY;
$initialbufferstate = ob_get_status();
$task = $this->get_test_adhoc_task();
logmanager::start_logging($task);
// Fetch the new output buffer state.
$state = ob_get_status();
// There will be no additional output buffer.
$this->assertNotEquals($initialbufferstate, $state);
}
/**
* When the logmode is set to fail, passing adhoc tests should not be logged.
*/
public function test_logmode_fail_with_passing_adhoc_task(): void {
global $CFG;
$this->resetAfterTest();
$CFG->task_logmode = logmanager::MODE_FAILONLY;
$logger = $this->get_mocked_logger();
$initialbufferstate = ob_get_status();
$task = $this->get_test_adhoc_task();
logmanager::start_logging($task);
manager::adhoc_task_complete($task);
$this->assertEmpty($logger::$storelogfortask);
}
/**
* When the logmode is set to fail, passing scheduled tests should not be logged.
*/
public function test_logmode_fail_with_passing_scheduled_task(): void {
global $CFG;
$this->resetAfterTest();
$CFG->task_logmode = logmanager::MODE_FAILONLY;
$logger = $this->get_mocked_logger();
$initialbufferstate = ob_get_status();
$task = $this->get_test_scheduled_task();
logmanager::start_logging($task);
manager::scheduled_task_complete($task);
$this->assertEmpty($logger::$storelogfortask);
}
/**
* When the logmode is set to fail, failing adhoc tests should be logged.
*/
public function test_logmode_fail_with_failing_adhoc_task(): void {
global $CFG;
$this->resetAfterTest();
// Mock the database. Marking jobs as failed updates a DB record which doesn't exist.
$this->mock_database();
$task = $this->get_test_adhoc_task();
$CFG->task_logmode = logmanager::MODE_FAILONLY;
$logger = $this->get_mocked_logger();
logmanager::start_logging($task);
manager::adhoc_task_failed($task);
$this->assertCount(1, $logger::$storelogfortask);
$this->assertEquals($task, $logger::$storelogfortask[0][0]);
$this->assertTrue($logger::$storelogfortask[0][2]);
}
/**
* When the logmode is set to fail, failing scheduled tests should be logged.
*/
public function test_logmode_fail_with_failing_scheduled_task(): void {
global $CFG;
$this->resetAfterTest();
// Mock the database. Marking jobs as failed updates a DB record which doesn't exist.
$this->mock_database();
$task = $this->get_test_scheduled_task();
$CFG->task_logmode = logmanager::MODE_FAILONLY;
$logger = $this->get_mocked_logger();
logmanager::start_logging($task);
manager::scheduled_task_failed($task);
$this->assertCount(1, $logger::$storelogfortask);
$this->assertEquals($task, $logger::$storelogfortask[0][0]);
$this->assertTrue($logger::$storelogfortask[0][2]);
}
/**
* When the logmode is set to fail, failing adhoc tests should be logged.
*/
public function test_logmode_any_with_failing_adhoc_task(): void {
global $CFG;
$this->resetAfterTest();
// Mock the database. Marking jobs as failed updates a DB record which doesn't exist.
$this->mock_database();
$task = $this->get_test_adhoc_task();
$CFG->task_logmode = logmanager::MODE_FAILONLY;
$logger = $this->get_mocked_logger();
logmanager::start_logging($task);
manager::adhoc_task_failed($task);
$this->assertCount(1, $logger::$storelogfortask);
$this->assertEquals($task, $logger::$storelogfortask[0][0]);
$this->assertTrue($logger::$storelogfortask[0][2]);
}
/**
* When the logmode is set to fail, failing scheduled tests should be logged.
*/
public function test_logmode_any_with_failing_scheduled_task(): void {
global $CFG;
$this->resetAfterTest();
// Mock the database. Marking jobs as failed updates a DB record which doesn't exist.
$this->mock_database();
$task = $this->get_test_scheduled_task();
$CFG->task_logmode = logmanager::MODE_FAILONLY;
$logger = $this->get_mocked_logger();
logmanager::start_logging($task);
manager::scheduled_task_failed($task);
$this->assertCount(1, $logger::$storelogfortask);
$this->assertEquals($task, $logger::$storelogfortask[0][0]);
$this->assertTrue($logger::$storelogfortask[0][2]);
}
/**
* When the logmode is set to fail, passing adhoc tests should be logged.
*/
public function test_logmode_any_with_passing_adhoc_task(): void {
global $CFG;
$this->resetAfterTest();
$this->mock_database();
$task = $this->get_test_adhoc_task();
$CFG->task_logmode = logmanager::MODE_ALL;
$logger = $this->get_mocked_logger();
logmanager::start_logging($task);
manager::adhoc_task_complete($task);
$this->assertCount(1, $logger::$storelogfortask);
$this->assertEquals($task, $logger::$storelogfortask[0][0]);
$this->assertFalse($logger::$storelogfortask[0][2]);
}
/**
* When the logmode is set to fail, passing scheduled tests should be logged.
*/
public function test_logmode_any_with_passing_scheduled_task(): void {
global $CFG;
$this->resetAfterTest();
$this->mock_database();
$task = $this->get_test_scheduled_task();
$CFG->task_logmode = logmanager::MODE_ALL;
$logger = $this->get_mocked_logger();
logmanager::start_logging($task);
manager::scheduled_task_complete($task);
$this->assertCount(1, $logger::$storelogfortask);
$this->assertEquals($task, $logger::$storelogfortask[0][0]);
$this->assertFalse($logger::$storelogfortask[0][2]);
}
/**
* Ensure that start_logging cannot be called in a nested fashion.
*/
public function test_prevent_nested_logging(): void {
$this->resetAfterTest();
$task = $this->get_test_adhoc_task();
logmanager::start_logging($task);
$this->expectException(\coding_exception::class);
logmanager::start_logging($task);
}
/**
* Ensure that logging can be called after a previous log has finished.
*/
public function test_repeated_usages(): void {
$this->resetAfterTest();
$logger = $this->get_mocked_logger();
$task = $this->get_test_adhoc_task();
logmanager::start_logging($task);
logmanager::finalise_log();
logmanager::start_logging($task);
logmanager::finalise_log();
$this->assertCount(2, $logger::$storelogfortask);
$this->assertEquals($task, $logger::$storelogfortask[0][0]);
$this->assertFalse($logger::$storelogfortask[0][2]);
$this->assertEquals($task, $logger::$storelogfortask[1][0]);
$this->assertFalse($logger::$storelogfortask[1][2]);
}
/**
* Enusre that when finalise_log is called when logging is not active, nothing happens.
*/
public function test_finalise_log_no_logging(): void {
$initialbufferstate = ob_get_status();
logmanager::finalise_log();
// There will be no additional output buffer.
$this->assertEquals($initialbufferstate, ob_get_status());
}
/**
* When log capture is enabled, calls to the flush function should cause log output to be both returned and captured.
*/
public function test_flush_on_own_buffer(): void {
$this->resetAfterTest();
$logger = $this->get_mocked_logger();
$testoutput = "I am the output under test.\n";
$task = $this->get_test_adhoc_task();
logmanager::start_logging($task);
echo $testoutput;
$this->expectOutputString($testoutput);
logmanager::flush();
// Finalise the log.
logmanager::finalise_log();
$this->assertCount(1, $logger::$storelogfortask);
$this->assertEquals($testoutput, file_get_contents($logger::$storelogfortask[0][1]));
}
/**
* When log capture is enabled, calls to the flush function should not affect any subsequent ob_start.
*/
public function test_flush_does_not_flush_inner_buffers(): void {
$this->resetAfterTest();
$logger = $this->get_mocked_logger();
$testoutput = "I am the output under test.\n";
$task = $this->get_test_adhoc_task();
logmanager::start_logging($task);
ob_start();
echo $testoutput;
ob_end_clean();
logmanager::flush();
// Finalise the log.
logmanager::finalise_log();
$this->assertCount(1, $logger::$storelogfortask);
// The task logger should not have captured the content of the inner buffer.
$this->assertEquals('', file_get_contents($logger::$storelogfortask[0][1]));
}
/**
* When log capture is enabled, calls to the flush function should not affect any subsequent ob_start.
*/
public function test_inner_flushed_buffers_are_logged(): void {
$this->resetAfterTest();
$logger = $this->get_mocked_logger();
$testoutput = "I am the output under test.\n";
$task = $this->get_test_adhoc_task();
logmanager::start_logging($task);
// We are going to flush the inner buffer. That means that we should expect the output immediately.
$this->expectOutputString($testoutput);
ob_start();
echo $testoutput;
ob_end_flush();
// Finalise the log.
logmanager::finalise_log();
$this->assertCount(1, $logger::$storelogfortask);
// The task logger should not have captured the content of the inner buffer.
$this->assertEquals($testoutput, file_get_contents($logger::$storelogfortask[0][1]));
}
/**
* Get an example adhoc task to use for testing.
*
* @return adhoc_task
*/
protected function get_test_adhoc_task(): adhoc_task {
$task = $this->getMockForAbstractClass(adhoc_task::class);
$task->set_component('core');
// Mock a lock on the task.
$lock = $this->getMockBuilder(\core\lock\lock::class)
->disableOriginalConstructor()
->getMock();
$task->set_lock($lock);
return $task;
}
/**
* Get an example scheduled task to use for testing.
*
* @return scheduled_task
*/
protected function get_test_scheduled_task(): scheduled_task {
$task = $this->getMockForAbstractClass(scheduled_task::class);
// Mock a lock on the task.
$lock = $this->getMockBuilder(\core\lock\lock::class)
->disableOriginalConstructor()
->getMock();
$task->set_lock($lock);
return $task;
}
/**
* Create and configure a mocked task logger.
*
* @return logging_test_mocked_logger
*/
protected function get_mocked_logger() {
global $CFG;
// We will modify config for the alternate logging class therefore we mnust reset after the test.
$this->resetAfterTest();
// Note PHPUnit does not support mocking static functions.
$CFG->task_log_class = logging_test_mocked_logger::class;
logging_test_mocked_logger::reset_test();
return $CFG->task_log_class;
}
/**
* Mock the database.
*/
protected function mock_database() {
global $DB;
// Store the old Database for restoration in reset.
$this->DB = $DB;
$DB = $this->getMockBuilder(\moodle_database::class)
->getMock();
$DB->method('get_record')
->willReturn((object) []);
}
}
/**
* Mocked logger.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class logging_test_mocked_logger implements task_logger {
/**
* @var bool Whether this is configured.
*/
public static $isconfigured = true;
/**
* @var array Arguments that store_log_for_task was called with.
*/
public static $storelogfortask = [];
/**
* @var bool Whether this logger has a report.
*/
public static $haslogreport = true;
/**
* Reset the test class.
*/
public static function reset_test() {
self::$isconfigured = true;
self::$storelogfortask = [];
self::$haslogreport = true;
}
/**
* Whether the task is configured and ready to log.
*
* @return bool
*/
public static function is_configured(): bool {
return self::$isconfigured;
}
/**
* Store the log for the specified task.
*
* @param task_base $task The task that the log belongs to.
* @param string $logpath The path to the log on disk
* @param bool $failed Whether the task failed
* @param int $dbreads The number of DB reads
* @param int $dbwrites The number of DB writes
* @param float $timestart The start time of the task
* @param float $timeend The end time of the task
*/
public static function store_log_for_task(task_base $task, string $logpath, bool $failed,
int $dbreads, int $dbwrites, float $timestart, float $timeend) {
self::$storelogfortask[] = func_get_args();
}
/**
* Whether this task logger has a report available.
*
* @return bool
*/
public static function has_log_report(): bool {
return self::$haslogreport;
}
/**
* Get any URL available for viewing relevant task log reports.
*
* @param string $classname The task class to fetch for
* @return \moodle_url
*/
public static function get_url_for_task_class(string $classname): \moodle_url {
return new \moodle_url('');
}
}
+350
View File
@@ -0,0 +1,350 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\task;
defined('MOODLE_INTERNAL') || die();
// We need to keep this here because there is a provider
// needing \core\task\adhoc_test_task and cannot move it
// to setUpBeforeClass() or similar. Whenever we allow to
// autoload fixtures, this can be removed.
require_once(__DIR__ . '/../fixtures/task_fixtures.php');
/**
* This file contains the unit tests for the task manager.
*
* @package core
* @category test
* @copyright 2019 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core\task\manager
*/
final class manager_test extends \advanced_testcase {
/**
* Data provider for test_get_candidate_adhoc_tasks.
*
* @return array
*/
public static function get_candidate_adhoc_tasks_provider(): array {
return [
[
'concurrencylimit' => 5,
'limit' => 100,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
],
'expected' => [
adhoc_test_task::class,
adhoc_test_task::class,
adhoc_test_task::class,
adhoc_test_task::class,
adhoc_test_task::class,
],
],
[
'concurrencylimit' => 5,
'limit' => 100,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
],
'expected' => [
adhoc_test_task::class,
adhoc_test_task::class,
adhoc_test_task::class,
adhoc_test_task::class,
],
],
[
'concurrencylimit' => 1,
'limit' => 100,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
],
'expected' => [],
],
[
'concurrencylimit' => 2,
'limit' => 100,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
],
'expected' => [],
],
[
'concurrencylimit' => 2,
'limit' => 100,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, time()),
new adhoc_test2_task(time() - 20, time()),
new adhoc_test2_task(time() - 20, time()),
new adhoc_test3_task(time() - 20, null),
],
'expected' => [adhoc_test3_task::class],
],
[
'concurrencylimit' => 2,
'limit' => 2,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test2_task(time() - 20, null),
],
'expected' => [
adhoc_test_task::class,
adhoc_test_task::class,
],
],
[
'concurrencylimit' => 2,
'limit' => 2,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, null),
new adhoc_test2_task(time() - 20, null),
],
'expected' => [
adhoc_test2_task::class,
],
],
[
'concurrencylimit' => 3,
'limit' => 100,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, null),
new adhoc_test2_task(time() - 20, time()),
new adhoc_test2_task(time() - 20, time()),
new adhoc_test2_task(time() - 20, null),
new adhoc_test3_task(time() - 20, time()),
new adhoc_test3_task(time() - 20, time()),
new adhoc_test3_task(time() - 20, null),
new adhoc_test4_task(time() - 20, time()),
new adhoc_test4_task(time() - 20, time()),
new adhoc_test4_task(time() - 20, null),
new adhoc_test5_task(time() - 20, time()),
new adhoc_test5_task(time() - 20, time()),
new adhoc_test5_task(time() - 20, null),
],
'expected' => [
adhoc_test_task::class,
adhoc_test2_task::class,
adhoc_test3_task::class,
adhoc_test4_task::class,
adhoc_test5_task::class,
],
],
[
'concurrencylimit' => 3,
'limit' => 100,
'pertasklimits' => [
'adhoc_test_task' => 2,
'adhoc_test2_task' => 2,
'adhoc_test3_task' => 2,
'adhoc_test4_task' => 2,
'adhoc_test5_task' => 2,
],
'tasks' => [
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, null),
new adhoc_test2_task(time() - 20, time()),
new adhoc_test2_task(time() - 20, time()),
new adhoc_test2_task(time() - 20, null),
new adhoc_test3_task(time() - 20, time()),
new adhoc_test3_task(time() - 20, time()),
new adhoc_test3_task(time() - 20, null),
new adhoc_test4_task(time() - 20, time()),
new adhoc_test4_task(time() - 20, time()),
new adhoc_test4_task(time() - 20, null),
new adhoc_test5_task(time() - 20, time()),
new adhoc_test5_task(time() - 20, time()),
new adhoc_test5_task(time() - 20, null),
],
'expected' => [],
],
];
}
/**
* Test that the candidate adhoc tasks are returned in the right order.
*
* @dataProvider get_candidate_adhoc_tasks_provider
*
* @param int $concurrencylimit The max number of runners each task can consume
* @param int $limit SQL limit
* @param array $pertasklimits Per-task limits
* @param array $tasks Array of tasks to put in DB and retrieve
* @param array $expected Array of expected classnames
*/
public function test_get_candidate_adhoc_tasks(
int $concurrencylimit,
int $limit,
array $pertasklimits,
array $tasks,
array $expected
): void {
$this->resetAfterTest();
foreach ($tasks as $task) {
manager::queue_adhoc_task($task);
}
$candidates = manager::get_candidate_adhoc_tasks(time(), $limit, $concurrencylimit, $pertasklimits);
$this->assertEquals(
array_map(
function (string $classname): string {
return '\\' . $classname;
},
$expected
),
array_column($candidates, 'classname')
);
}
/**
* Test that adhoc tasks are set as failed when shutdown is called during execution.
*/
public function test_adhoc_task_running_will_fail_when_shutdown(): void {
$this->resetAfterTest();
$this->preventResetByRollback();
$task1 = new adhoc_test_task();
$task1->set_next_run_time(time() - 20);
manager::queue_adhoc_task($task1);
$next1 = manager::get_next_adhoc_task(time());
\core\task\manager::adhoc_task_starting($next1);
self::assertEmpty(manager::get_failed_adhoc_tasks());
// Trigger shutdown handler.
\core_shutdown_manager::shutdown_handler();
$failedtasks = manager::get_failed_adhoc_tasks();
self::assertCount(1, $failedtasks);
self::assertEquals($next1->get_id(), $failedtasks[0]->get_id());
}
/**
* Test that scheduled tasks are set as failed when shutdown is called during execution.
*/
public function test_scheduled_task_running_will_fail_when_shutdown(): void {
global $DB;
$this->resetAfterTest();
$this->preventResetByRollback();
// Disable all the tasks, so we can insert our own and be sure it's the only one being run.
$DB->set_field('task_scheduled', 'disabled', 1);
$task1 = new scheduled_test_task();
$task1->set_minute('*');
$task1->set_next_run_time(time() - HOURSECS);
$DB->insert_record('task_scheduled', manager::record_from_scheduled_task($task1));
$next1 = \core\task\manager::get_next_scheduled_task(time());
\core\task\manager::scheduled_task_starting($next1);
$running = manager::get_running_tasks();
$this->assertCount(1, $running);
// Trigger shutdown handler.
\core_shutdown_manager::shutdown_handler();
$running = manager::get_running_tasks();
$this->assertCount(0, $running);
$scheduledtask1 = manager::get_scheduled_task(scheduled_test_task::class);
self::assertGreaterThan($next1->get_fail_delay(), $scheduledtask1->get_fail_delay());
}
public function test_get_next_adhoc_task_will_respect_failed_tasks(): void {
// Create three tasks, one is burned on the first get_next_adhoc_task() call to build up the cache,
// the second will be set to failed and the third is required to make the "uniquetasksinqueue" query
// within the get_next_adhoc_task() function not returning a different count of remaining unique tasks.
manager::queue_adhoc_task(new adhoc_test_task());
manager::queue_adhoc_task(new adhoc_test_task());
manager::queue_adhoc_task(new adhoc_test_task());
$timestart = time();
$candidates = manager::get_candidate_adhoc_tasks($timestart, 4, null);
$this->assertEquals(count($candidates), 3);
$task1 = manager::adhoc_task_from_record(array_shift($candidates));
$task2 = manager::adhoc_task_from_record(array_shift($candidates));
$task3 = manager::adhoc_task_from_record(array_shift($candidates));
// Build up the cache by getting the first task.
$task = manager::get_next_adhoc_task($timestart);
// Release the lock by completing the task to avoid "A lock was created but not released" error if the assertion fails.
manager::adhoc_task_complete($task);
$this->assertEquals($task->get_id(), $task1->get_id());
// Make $task2 failed...
try {
// Expecting "Error: Call to a member function release() on null" as the task was not locked before.
manager::adhoc_task_failed($task2);
} catch (\Throwable $t) {
// Ignoring "Call to a member function release() on null" and throw anything else.
if ($t->getMessage() != "Call to a member function release() on null") {
throw $t;
}
}
$task = manager::get_next_adhoc_task($timestart);
// Release the lock by completing the task to avoid "A lock was created but not released" error if the assertion fails.
manager::adhoc_task_complete($task);
// Task $task2 should not be returned because it has failed meanwhile and
// therefore has its nextruntime in the future...
$this->assertNotEquals($task->get_id(), $task2->get_id());
// Just to make sure check that the complete queue is as expected.
$this->assertEquals($task->get_id(), $task3->get_id());
// Now the queue should be empty...
$task = manager::get_next_adhoc_task($timestart);
$this->assertNull($task);
$this->resetAfterTest();
}
}
+158
View File
@@ -0,0 +1,158 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\task;
/**
* This file contains unit tests for the 'task running' data.
*
* @package core
* @category test
* @copyright 2019 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class running_test extends \advanced_testcase {
public static function setUpBeforeClass(): void {
require_once(__DIR__ . '/../fixtures/task_fixtures.php');
}
/**
* Test for ad-hoc tasks.
*/
public function test_adhoc_task_running(): void {
$this->resetAfterTest();
// Specify lock factory. The reason is that Postgres locks don't work within a single
// process (i.e. if you try to get a lock that you already locked, it will just let you)
// which is usually OK but not here where we are simulating running two tasks at once in
// the same process.
set_config('lock_factory', '\core\lock\db_record_lock_factory');
// Create and queue 2 new ad-hoc tasks.
$task1 = new adhoc_test_task();
$task1->set_next_run_time(time() - 20);
manager::queue_adhoc_task($task1);
$task2 = new adhoc_test2_task();
$task2->set_next_run_time(time() - 10);
manager::queue_adhoc_task($task2);
// Check no tasks are marked running.
$running = manager::get_running_tasks();
$this->assertEmpty($running);
// Mark the first task running and check results.
$before = time();
$next1 = manager::get_next_adhoc_task(time());
manager::adhoc_task_starting($next1);
$after = time();
$running = manager::get_running_tasks();
$this->assertCount(1, $running);
foreach ($running as $item) {
$this->assertEquals('adhoc', $item->type);
$this->assertLessThanOrEqual($after, $item->timestarted);
$this->assertGreaterThanOrEqual($before, $item->timestarted);
}
// Mark the second task running and check results.
$next2 = manager::get_next_adhoc_task(time());
manager::adhoc_task_starting($next2);
$running = manager::get_running_tasks();
$this->assertCount(2, $running);
// Second task completes successfully.
manager::adhoc_task_complete($next2);
$running = manager::get_running_tasks();
$this->assertCount(1, $running);
// First task fails.
manager::adhoc_task_failed($next1);
$running = manager::get_running_tasks();
$this->assertCount(0, $running);
}
/**
* Test for scheduled tasks.
*/
public function test_scheduled_task_running(): void {
global $DB;
$this->resetAfterTest();
// Check no tasks are marked running.
$running = manager::get_running_tasks();
$this->assertEmpty($running);
// Disable all the tasks, except two, and set those two due to run.
$DB->set_field_select('task_scheduled', 'disabled', 1, 'classname != ? AND classname != ?',
['\core\task\session_cleanup_task', '\core\task\file_trash_cleanup_task']);
$DB->set_field('task_scheduled', 'nextruntime', 1,
['classname' => '\core\task\session_cleanup_task']);
$DB->set_field('task_scheduled', 'nextruntime', 1,
['classname' => '\core\task\file_trash_cleanup_task']);
$DB->set_field('task_scheduled', 'lastruntime', time() - 1000,
['classname' => '\core\task\session_cleanup_task']);
$DB->set_field('task_scheduled', 'lastruntime', time() - 500,
['classname' => '\core\task\file_trash_cleanup_task']);
// Get the first task and start it off.
$next1 = manager::get_next_scheduled_task(time());
$before = time();
manager::scheduled_task_starting($next1);
$after = time();
$running = manager::get_running_tasks();
$this->assertCount(1, $running);
foreach ($running as $item) {
$this->assertLessThanOrEqual($after, $item->timestarted);
$this->assertGreaterThanOrEqual($before, $item->timestarted);
$this->assertEquals('\core\task\session_cleanup_task', $item->classname);
}
// Mark the second task running and check results. We have to change the times so the other
// one comes up first, otherwise it repeats the same one.
$DB->set_field('task_scheduled', 'lastruntime', time() - 1500,
['classname' => '\core\task\file_trash_cleanup_task']);
// Make sure that there is a time gap between task to sort them as expected.
sleep(1);
$next2 = manager::get_next_scheduled_task(time());
manager::scheduled_task_starting($next2);
// Check default sorting by timestarted.
$running = manager::get_running_tasks();
$this->assertCount(2, $running);
$item = array_shift($running);
$this->assertEquals('\core\task\session_cleanup_task', $item->classname);
$item = array_shift($running);
$this->assertEquals('\core\task\file_trash_cleanup_task', $item->classname);
// Check sorting by time ASC.
$running = manager::get_running_tasks('time ASC');
$this->assertCount(2, $running);
$item = array_shift($running);
$this->assertEquals('\core\task\file_trash_cleanup_task', $item->classname);
$item = array_shift($running);
$this->assertEquals('\core\task\session_cleanup_task', $item->classname);
// Complete the file trash one.
manager::scheduled_task_complete($next2);
$running = manager::get_running_tasks();
$this->assertCount(1, $running);
// Other task fails.
manager::scheduled_task_failed($next1);
$running = manager::get_running_tasks();
$this->assertCount(0, $running);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,310 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\task;
use moodle_url;
/**
* Contains tests for login related notifications.
*
* @package core
* @copyright 2021 Juan Leyva <juan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core\task\send_login_notifications
*/
class send_login_notifications_test extends \advanced_testcase {
/**
* Test new login notification.
*/
public function test_login_notification(): void {
global $SESSION;
$this->resetAfterTest();
$loginuser = self::getDataGenerator()->create_user();
$this->setUser(0);
// Mock data for test.
$loginuser->lastip = '1.2.3.4.6'; // Different ip that current.
$SESSION->isnewsessioncookie = true; // New session cookie.
@complete_user_login($loginuser);
// Redirect messages to sink and stop buffer output from CLI task.
$sink = $this->redirectMessages();
ob_start();
$this->runAdhocTasks('\core\task\send_login_notifications');
$output = ob_get_contents();
ob_end_clean();
$messages = $sink->get_messages();
$sink->close();
// Send notification, new IP and new session.
$this->assertCount(1, $messages);
$this->assertEquals($loginuser->id, $messages[0]->useridto);
$this->assertEquals('newlogin', $messages[0]->eventtype);
}
/**
* Test new login notification is skipped because of same IP from last login.
*/
public function test_login_notification_skip_same_ip(): void {
global $SESSION;
$this->resetAfterTest();
$loginuser = self::getDataGenerator()->create_user();
$this->setUser(0);
// Mock data for test.
$SESSION->isnewsessioncookie = true; // New session cookie.
@complete_user_login($loginuser);
// Redirect messages to sink and stop buffer output from CLI task.
$sink = $this->redirectMessages();
ob_start();
$this->runAdhocTasks('\core\task\send_login_notifications');
$output = ob_get_contents();
ob_end_clean();
$messages = $sink->get_messages();
$sink->close();
// Skip notification when we have the same previous IP even if the browser used to connect is new.
$this->assertCount(0, $messages);
}
/**
* Test new login notification is skipped because of same browser from last login.
*/
public function test_login_notification_skip_same_browser(): void {
global $SESSION;
$this->resetAfterTest();
$loginuser = self::getDataGenerator()->create_user();
$this->setUser(0);
// Mock data for test.
$loginuser->lastip = '1.2.3.4.6'; // Different ip that current.
$SESSION->isnewsessioncookie = false;
@complete_user_login($loginuser);
// Redirect messages to sink and stop buffer output from CLI task.
$sink = $this->redirectMessages();
ob_start();
$this->runAdhocTasks('\core\task\send_login_notifications');
$output = ob_get_contents();
ob_end_clean();
$messages = $sink->get_messages();
$sink->close();
// Skip notification, different ip but same browser (probably, mobile phone browser).
$this->assertCount(0, $messages);
}
/**
* Test new login notification is skipped because of auto-login from the mobile app (skip duplicated notifications).
*/
public function test_login_notification_skip_mobileapp(): void {
global $SESSION;
$this->resetAfterTest();
$loginuser = self::getDataGenerator()->create_user();
$this->setUser(0);
// Mock data for test.
$loginuser->lastip = '1.2.3.4.6'; // Different ip that current.
$SESSION->isnewsessioncookie = true; // New session cookie.
\core_useragent::instance(true, 'MoodleMobile'); // Force fake mobile app user agent.
@complete_user_login($loginuser);
// Redirect messages to sink and stop buffer output from CLI task.
$sink = $this->redirectMessages();
ob_start();
$this->runAdhocTasks('\core\task\send_login_notifications');
$output = ob_get_contents();
ob_end_clean();
$messages = $sink->get_messages();
$sink->close();
$this->assertCount(0, $messages);
}
/**
* Test new login notification where the user auth method provides a custom change password URL
*/
public function test_login_notification_custom_change_password_url(): void {
global $SESSION;
$this->resetAfterTest();
$this->setUser(0);
// Set LDAP auth change password URL.
$changepasswordurl = (new moodle_url('/changepassword.php'))->out(false);
set_config('changepasswordurl', $changepasswordurl, 'auth_ldap');
$ldapuser = $this->getDataGenerator()->create_user(['auth' => 'ldap']);
// Mock data for test.
$ldapuser->lastip = '1.2.3.4';
$SESSION->isnewsessioncookie = true;
@complete_user_login($ldapuser);
// Redirect messages to sink and stop buffer output from CLI task.
$sink = $this->redirectMessages();
ob_start();
$this->runAdhocTasks(send_login_notifications::class);
ob_end_clean();
$messages = $sink->get_messages();
$sink->close();
// Send notification, assert custom change password URL is present.
$this->assertCount(1, $messages);
$this->assertStringContainsString("If you don't recognise this activity, please " .
"<a href=\"{$changepasswordurl}\">change your password</a>.", $messages[0]->fullmessagehtml);
}
/**
* Test new mobile app login notification.
*/
public function test_mobile_app_login_notification(): void {
global $USER, $DB, $SESSION;
$this->resetAfterTest();
$loginuser = self::getDataGenerator()->create_user();
$this->setUser($loginuser);
// Mock data for test.
$USER->lastip = '1.2.3.4.6'; // Different ip that current.
$service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
$token = \core_external\util::generate_token_for_current_user($service);
\core_useragent::instance(true, 'MoodleMobile'); // Force fake mobile app user agent.
// Simulate we are using an new device.
$fakedevice = (object) [
'userid' => $USER->id,
'appid' => 'com.moodle.moodlemobile',
'name' => 'occam',
'model' => 'Nexus 4',
'platform' => 'Android',
'version' => '4.2.2',
'pushid' => 'kishUhd',
'uuid' => 'KIhud7s',
'timecreated' => time() + MINSECS,
'timemodified' => time() + MINSECS
];
$DB->insert_record('user_devices', $fakedevice);
\core_external\util::log_token_request($token);
// Redirect messages to sink and stop buffer output from CLI task.
$sink = $this->redirectMessages();
ob_start();
$this->runAdhocTasks('\core\task\send_login_notifications');
$output = ob_get_contents();
ob_end_clean();
$messages = $sink->get_messages();
$sink->close();
// We sent a login notification because we are using a new device and different IP.
$this->assertCount(1, $messages);
$this->assertEquals($loginuser->id, $messages[0]->useridto);
$this->assertEquals('newlogin', $messages[0]->eventtype);
}
/**
* Test new mobile app login notification skipped becase of same last ip.
*/
public function test_mobile_app_login_notification_skip_same_ip(): void {
global $USER, $DB, $SESSION;
$this->resetAfterTest();
$loginuser = self::getDataGenerator()->create_user();
$this->setUser($loginuser);
// Mock data for test.
$USER->lastip = '0.0.0.0';
$service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
$token = \core_external\util::generate_token_for_current_user($service);
\core_useragent::instance(true, 'MoodleMobile'); // Force fake mobile app user agent.
// Simulate we are using an new device.
$fakedevice = (object) [
'userid' => $USER->id,
'appid' => 'com.moodle.moodlemobile',
'name' => 'occam',
'model' => 'Nexus 4',
'platform' => 'Android',
'version' => '4.2.2',
'pushid' => 'kishUhd',
'uuid' => 'KIhud7s',
'timecreated' => time() + MINSECS,
'timemodified' => time() + MINSECS
];
$DB->insert_record('user_devices', $fakedevice);
\core_external\util::log_token_request($token);
// Redirect messages to sink and stop buffer output from CLI task.
$sink = $this->redirectMessages();
ob_start();
$this->runAdhocTasks('\core\task\send_login_notifications');
$output = ob_get_contents();
ob_end_clean();
$messages = $sink->get_messages();
$sink->close();
// While using the same IP avoid sending new login notifications even if we are using a new device.
$this->assertCount(0, $messages);
}
/**
* Test new mobile app login notification skipped becase of same device.
*/
public function test_mobile_app_login_notification_skip_same_device(): void {
global $USER, $DB, $SESSION;
$this->resetAfterTest();
$loginuser = self::getDataGenerator()->create_user();
$this->setUser($loginuser);
// Mock data for test.
$USER->lastip = '1.2.3.4.6'; // New ip.
$service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
$token = \core_external\util::generate_token_for_current_user($service);
\core_useragent::instance(true, 'MoodleMobile'); // Force fake mobile app user agent.
\core_external\util::log_token_request($token);
// Redirect messages to sink and stop buffer output from CLI task.
$sink = $this->redirectMessages();
ob_start();
$this->runAdhocTasks('\core\task\send_login_notifications');
$output = ob_get_contents();
ob_end_clean();
$messages = $sink->get_messages();
$sink->close();
// While using the same device avoid sending new login notifications even if the IP changes.
$this->assertCount(0, $messages);
}
}
@@ -0,0 +1,166 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\task;
use advanced_testcase;
/**
* Class containing unit tests for the show started courses task.
*
* It automatically sets the course visibility to shown when the course start date matches the current day.
*
* @package core
* @copyright 2023 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core\task\show_started_courses_task
*/
class show_started_courses_task_test extends advanced_testcase {
/**
* Test show_started_courses cron task.
*
* @dataProvider get_courses_provider
* @covers ::execute
*
* @param int $lastweekhidden Number of courses with the start date set to last week to be created.
* @param int $yesterdayhidden Number of courses with the start date set to yesterday to be created.
* @param int $tomorrowhidden Number of courses with the start date set to tomorrow to be created.
* @param bool $createvisible Whether visible courses should be created or not.
*/
public function test_show_started_courses(
int $lastweekhidden,
int $yesterdayhidden,
int $tomorrowhidden,
bool $createvisible = true
): void {
global $DB;
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$visiblecourses = [];
$hiddencourses = [];
$now = time();
$lastweek = $now - WEEKSECS;
$yesterday = $now - DAYSECS + 60;
$tomorrow = $now + DAYSECS;
// Hidden course that started last week.
for ($i = 0; $i < $lastweekhidden; $i++) {
$generator->create_course(['visible' => false, 'startdate' => $lastweek]);
}
// Hidden course that started yesterday.
for ($i = 0; $i < $yesterdayhidden; $i++) {
$hiddencourses[] = $generator->create_course(['visible' => false, 'startdate' => $yesterday])->id;
}
// Hidden course that hasn't started yet.
for ($i = 0; $i < $tomorrowhidden; $i++) {
$generator->create_course(['visible' => false, 'startdate' => $tomorrow]);
}
if ($createvisible) {
// Visible course that already started.
$visiblecourses[] = $generator->create_course(['visible' => true, 'startdate' => $yesterday])->id;
// Visible course that hasn't started yet.
$visiblecourses[] = $generator->create_course(['visible' => true, 'startdate' => $tomorrow])->id;
}
$visibletotal = count($visiblecourses) + 1;
$coursetotal = $visibletotal + $lastweekhidden + $yesterdayhidden + $tomorrowhidden;
// Check current courses have been created correctly.
$this->assertEquals($coursetotal, $DB->count_records('course'));
$this->assertEquals($visibletotal, $DB->count_records('course', ['visible' => 1]));
$sink = $this->redirectEvents();
// Run the show started courses task.
ob_start();
$task = new show_started_courses_task();
$task->execute();
ob_end_clean();
// Confirm the courses with yesterday as starting date are visible too. The rest should remain hidden.
$this->assertEquals($coursetotal, $DB->count_records('course'));
$courses = $DB->get_records('course', ['visible' => 1], '', 'id');
$this->assertCount($visibletotal + $yesterdayhidden, $courses);
$expected = array_merge($hiddencourses, $visiblecourses);
$this->assertEquals(asort($expected), asort($courses));
// Check the started course event has been raised.
$events = $sink->get_events();
$sink->close();
$this->assertCount($yesterdayhidden, $events);
foreach ($events as $event) {
$this->assertInstanceOf('\\core\\event\\course_started', $event);
$this->assertArrayHasKey($event->courseid, array_flip($expected));
}
}
/**
* Data provider for test_show_started_courses.
*
* @return array
*/
public function get_courses_provider(): array {
return [
'No hidden courses' => [
'lastweek' => 0,
'yesterday' => 0,
'tomorrow' => 0,
],
'No hidden courses (without visible courses)' => [
'lastweek' => 0,
'yesterday' => 0,
'tomorrow' => 0,
'createvisible' => false,
],
'Hidden courses with last week or tomorrow dates' => [
'lastweek' => 2,
'yesterday' => 0,
'tomorrow' => 2,
],
'One hidden course of each type (last week, yesterday and tomorrow)' => [
'lastweek' => 1,
'yesterday' => 1,
'tomorrow' => 1,
],
'Different hidden courses of each type' => [
'lastweek' => 2,
'yesterday' => 3,
'tomorrow' => 4,
],
'A couple of hidden courses of each type (without visible courses)' => [
'lastweek' => 2,
'yesterday' => 2,
'tomorrow' => 2,
'createvisible' => false,
],
'Only a few hidden courses for yesterday' => [
'lastweek' => 0,
'yesterday' => 5,
'tomorrow' => 0,
],
'Only a few hidden courses for yesterday (without visible courses)' => [
'lastweek' => 0,
'yesterday' => 5,
'tomorrow' => 0,
'createvisible' => false,
],
];
}
}