AutorÃa | Ultima modificación | Ver Log |
<?php
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
namespace core\hook;
use core\di;
/**
* Hooks tests.
*
* @package core
* @author Petr Skoda
* @copyright 2022 Open LMS
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core\hook\manager
*/
final class manager_test extends \advanced_testcase {
/**
* Test public factory method to get hook manager.
*/
public function test_get_instance(): void {
$manager = manager::get_instance();
$this->assertInstanceOf(manager::class, $manager);
$this->assertSame($manager, manager::get_instance());
}
/**
* Test getting of manager test instance.
*/
public function test_phpunit_get_instance(): void {
$testmanager = manager::phpunit_get_instance([]);
$this->assertSame([], $testmanager->get_hooks_with_callbacks());
// We get a new instance every time.
$this->assertNotSame($testmanager, manager::phpunit_get_instance([]));
$componentfiles = [
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
];
$testmanager = manager::phpunit_get_instance($componentfiles);
$this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
}
/**
* Test loading and parsing of callbacks from files.
*/
public function test_callbacks(): void {
$componentfiles = [
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
];
$testmanager = manager::phpunit_get_instance($componentfiles);
$this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
$callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
$this->assertCount(2, $callbacks);
$this->assertSame([
'callback' => 'test_plugin\\callbacks::test2',
'component' => 'test_plugin2',
'disabled' => false,
'priority' => 200,
], $callbacks[0]);
$this->assertSame([
'callback' => 'test_plugin\\callbacks::test1',
'component' => 'test_plugin1',
'disabled' => false,
'priority' => 100,
], $callbacks[1]);
$this->assertDebuggingNotCalled();
$componentfiles = [
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_broken.php',
];
$testmanager = manager::phpunit_get_instance($componentfiles);
$this->assertSame([], $testmanager->get_hooks_with_callbacks());
$debuggings = $this->getDebuggingMessages();
$this->resetDebugging();
$this->assertSame(
'Hook callback definition requires \'hook\' name in \'test_plugin1\'',
$debuggings[0]->message
);
$this->assertSame(
'Hook callback definition requires \'callback\' callable in \'test_plugin1\'',
$debuggings[1]->message
);
$this->assertSame(
'Hook callback definition contains invalid \'callback\' static class method string in \'test_plugin1\'',
$debuggings[2]->message
);
$this->assertCount(3, $debuggings);
}
/**
* Test hook dispatching, that is callback execution.
*/
public function test_dispatch(): void {
require_once(__DIR__ . '/../fixtures/hook/hook.php');
require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
$componentfiles = [
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
];
$testmanager = manager::phpunit_get_instance($componentfiles);
\test_plugin\callbacks::$calls = [];
$hook = new \test_plugin\hook\hook();
$result = $testmanager->dispatch($hook);
$this->assertSame($hook, $result);
$this->assertSame(['test2', 'test1'], \test_plugin\callbacks::$calls);
\test_plugin\callbacks::$calls = [];
$this->assertDebuggingNotCalled();
}
/**
* Test hook dispatching, that is callback execution.
*/
public function test_dispatch_with_exception(): void {
require_once(__DIR__ . '/../fixtures/hook/hook.php');
require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
$componentfiles = [
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_exception.php',
'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
];
$testmanager = manager::phpunit_get_instance($componentfiles);
$hook = new \test_plugin\hook\hook();
$this->expectException(\Exception::class);
$this->expectExceptionMessage('grrr');
$testmanager->dispatch($hook);
}
/**
* Test hook dispatching, that is callback execution.
*/
public function test_dispatch_with_invalid(): void {
require_once(__DIR__ . '/../fixtures/hook/hook.php');
require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
// Missing callbacks is ignored.
$componentfiles = [
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_missing.php',
'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
];
$testmanager = manager::phpunit_get_instance($componentfiles);
\test_plugin\callbacks::$calls = [];
$hook = new \test_plugin\hook\hook();
$testmanager->dispatch($hook);
$this->assertDebuggingCalled(
"Hook callback definition contains invalid 'callback' method name in 'test_plugin1'. Callback method not found.",
);
$this->assertSame(['test2'], \test_plugin\callbacks::$calls);
}
/**
* Test stoppping of hook dispatching.
*/
public function test_dispatch_stoppable(): void {
require_once(__DIR__ . '/../fixtures/hook/stoppablehook.php');
require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
$componentfiles = [
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_stoppable.php',
'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_stoppable.php',
];
$testmanager = manager::phpunit_get_instance($componentfiles);
\test_plugin\callbacks::$calls = [];
$hook = new \test_plugin\hook\stoppablehook();
$result = $testmanager->dispatch($hook);
$this->assertSame($hook, $result);
$this->assertSame(['stop1'], \test_plugin\callbacks::$calls);
\test_plugin\callbacks::$calls = [];
$this->assertDebuggingNotCalled();
}
/**
* Tests callbacks can be overridden via CFG settings.
*/
public function test_callback_overriding(): void {
global $CFG;
$this->resetAfterTest();
$componentfiles = [
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
];
$testmanager = manager::phpunit_get_instance($componentfiles);
$this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
$callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
$this->assertCount(2, $callbacks);
$this->assertSame([
'callback' => 'test_plugin\\callbacks::test2',
'component' => 'test_plugin2',
'disabled' => false,
'priority' => 200,
], $callbacks[0]);
$this->assertSame([
'callback' => 'test_plugin\\callbacks::test1',
'component' => 'test_plugin1',
'disabled' => false,
'priority' => 100,
], $callbacks[1]);
$CFG->hooks_callback_overrides = [
'test_plugin\\hook\\hook' => [
'test_plugin\\callbacks::test2' => ['priority' => 33],
],
];
$testmanager = manager::phpunit_get_instance($componentfiles);
$this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
$callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
$this->assertCount(2, $callbacks);
$this->normalise_callbacks($callbacks);
$this->assertSame([
'callback' => 'test_plugin\\callbacks::test1',
'component' => 'test_plugin1',
'disabled' => false,
'priority' => 100,
], $callbacks[0]);
$this->assertSame([
'callback' => 'test_plugin\\callbacks::test2',
'component' => 'test_plugin2',
'defaultpriority' => 200,
'disabled' => false,
'priority' => 33,
], $callbacks[1]);
$CFG->hooks_callback_overrides = [
'test_plugin\\hook\\hook' => [
'test_plugin\\callbacks::test2' => ['priority' => 33, 'disabled' => true],
],
];
$testmanager = manager::phpunit_get_instance($componentfiles);
$this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
$callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
$this->assertCount(2, $callbacks);
$this->normalise_callbacks($callbacks);
$this->assertSame([
'callback' => 'test_plugin\\callbacks::test1',
'component' => 'test_plugin1',
'disabled' => false,
'priority' => 100,
], $callbacks[0]);
$this->assertSame([
'callback' => 'test_plugin\\callbacks::test2',
'component' => 'test_plugin2',
'defaultpriority' => 200,
'disabled' => true,
'priority' => 33,
], $callbacks[1]);
$CFG->hooks_callback_overrides = [
'test_plugin\\hook\\hook' => [
'test_plugin\\callbacks::test2' => ['disabled' => true],
],
];
$testmanager = manager::phpunit_get_instance($componentfiles);
$this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
$callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
$this->assertCount(2, $callbacks);
$this->assertSame([
'callback' => 'test_plugin\\callbacks::test2',
'component' => 'test_plugin2',
'disabled' => true,
'priority' => 200,
], $callbacks[0]);
$this->assertSame([
'callback' => 'test_plugin\\callbacks::test1',
'component' => 'test_plugin1',
'disabled' => false,
'priority' => 100,
], $callbacks[1]);
require_once(__DIR__ . '/../fixtures/hook/hook.php');
require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
\test_plugin\callbacks::$calls = [];
$hook = new \test_plugin\hook\hook();
$result = $testmanager->dispatch($hook);
$this->assertSame($hook, $result);
$this->assertSame(['test1'], \test_plugin\callbacks::$calls);
\test_plugin\callbacks::$calls = [];
$this->assertDebuggingNotCalled();
$CFG->hooks_callback_overrides = [];
}
/**
* Register a fake plugin called hooktest in the component manager.
*
* Tests consuming this helpers must run in a separate process.
*/
protected function setup_hooktest_plugin(): void {
global $CFG;
$this->add_mocked_plugintype('fake', "{$CFG->dirroot}/lib/tests/fixtures/hook/fakeplugins");
$this->add_mocked_plugin('fake', 'hooktest', "{$CFG->dirroot}/lib/tests/fixtures/hook/fakeplugins/hooktest");
}
/**
* Call a plugin callback that has been replaced by a hook, but has no hook callback.
*
* The original callback should be called, but a debugging message should be output.
*
* @runInSeparateProcess
*/
public function test_migrated_callback(): void {
$this->resetAfterTest(true);
// Include plugin hook discovery agent, and the hook that replaces the callback.
require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php');
require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_callback.php');
// Register the fake plugin with the component manager.
$this->setup_hooktest_plugin();
// Register the fake plugin with the hook manager, but don't define any hook callbacks.
di::set(
manager::class,
manager::phpunit_get_instance(
[
'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks_nocallbacks.php',
],
),
);
// Confirm a non-deprecated callback is called as expected.
$this->assertEquals('Called current callback', component_callback('fake_hooktest', 'current_callback'));
// Confirm the deprecated callback is called as expected.
$this->assertEquals(
'Called deprecated callback',
component_callback('fake_hooktest', 'old_callback', [], null, true)
);
$this->assertDebuggingCalled(
'Callback old_callback in fake_hooktest component should be migrated to new hook ' .
'callback for fake_hooktest\hook\hook_replacing_callback'
);
}
/**
* Call a plugin callback that has been replaced by a hook, and has a hook callback.
*
* The original callback should not be called, and no debugging should be output.
*
* @runInSeparateProcess
*/
public function test_migrated_callback_with_replacement(): void {
$this->resetAfterTest(true);
// Include plugin hook discovery agent, and the hook that replaces the callback, and a hook callback for the hook.
require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php');
require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_callback.php');
require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook_callbacks.php');
// Register the fake plugin with the component manager.
$this->setup_hooktest_plugin();
// Register the fake plugin with the hook manager, including the hook callback.
di::set(
manager::class,
manager::phpunit_get_instance([
'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks.php',
]),
);
// Confirm a non-deprecated callback is called as expected.
$this->assertEquals('Called current callback', component_callback('fake_hooktest', 'current_callback'));
// Confirm the deprecated callback is not called, as expected.
$this->assertNull(component_callback('fake_hooktest', 'old_callback', [], null, true));
$this->assertDebuggingNotCalled();
}
/**
* Call a plugin class callback that has been replaced by a hook, but has no hook callback.
*
* The original class callback should be called, but a debugging message should be output.
*
* @runInSeparateProcess
*/
public function test_migrated_class_callback(): void {
$this->resetAfterTest(true);
// Include plugin hook discovery agent, the class containing callbacks, and the hook that replaces the class callback.
require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/callbacks.php');
require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php');
require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_class_callback.php');
// Register the fake plugin with the component manager.
$this->setup_hooktest_plugin();
// Register the fake plugin with the hook manager, but don't define any hook callbacks.
di::set(
manager::class,
manager::phpunit_get_instance([
'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks_nocallbacks.php',
]),
);
// Confirm a non-deprecated class callback is called as expected.
$this->assertEquals(
'Called current class callback',
component_class_callback('fake_hooktest\callbacks', 'current_class_callback', [])
);
// Confirm the deprecated class callback is called as expected.
$this->assertEquals(
'Called deprecated class callback',
component_class_callback('fake_hooktest\callbacks', 'old_class_callback', [], null, true)
);
$this->assertDebuggingCalled(
'Callback callbacks::old_class_callback in fake_hooktest component should be migrated to new hook ' .
'callback for fake_hooktest\hook\hook_replacing_class_callback'
);
}
/**
* Call a plugin class callback that has been replaced by a hook, and has a hook callback.
*
* The original callback should not be called, and no debugging should be output.
*
* @runInSeparateProcess
*/
public function test_migrated_class_callback_with_replacement(): void {
$this->resetAfterTest(true);
// Include plugin hook discovery agent, the class containing callbacks, the hook that replaces the class callback,
// and a hook callback for the new hook.
require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/callbacks.php');
require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php');
require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_class_callback.php');
require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook_callbacks.php');
// Register the fake plugin with the component manager.
$this->setup_hooktest_plugin();
// Register the fake plugin with the hook manager, including the hook callback.
di::set(
manager::class,
manager::phpunit_get_instance([
'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks.php',
]),
);
// Confirm a non-deprecated class callback is called as expected.
$this->assertEquals(
'Called current class callback',
component_class_callback('fake_hooktest\callbacks', 'current_class_callback', [])
);
// Confirm the deprecated class callback is not called, as expected.
$this->assertNull(component_class_callback('fake_hooktest\callbacks', 'old_class_callback', [], null, true));
$this->assertDebuggingNotCalled();
}
/**
* Normalise the sort order of callbacks to help with asserts.
*
* @param array $callbacks
*/
private function normalise_callbacks(array &$callbacks): void {
foreach ($callbacks as &$callback) {
ksort($callback);
}
}
}