AutorÃa | Ultima modificación | Ver Log |
<?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\tests;
/**
* Trait support full/deep plugin/subplugin mocking, forcing \core\component to rebuild in full.
*
* Useful for testing core\component, plugin managers, and lower level classes of that nature.
*
* For other plugin mocking, shallow mocking may be more suitable. See:
* {@see \advanced_testcase::add_mocked_plugintype}
* {@see \advanced_testcase::add_mocked_plugin()}
*
* @package core
* @category phpunit
* @copyright 2025 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait fake_plugins_test_trait {
/** @var bool Whether to fully reset the component cache after each test */
protected bool $fullcomponentreset = false;
/**
* Reset the component cache.
*
* This is an After test method that supplements the test tearDown to ensure the component cache is reset.
*
* Please note that the component cache is always partially reset in the main tearDown method.
* This method will extend the reset to also cause re-reads off disk.
*/
#[\PHPUnit\Framework\Attributes\After]
public function reset_component_cache(): void {
if ($this->fullcomponentreset === true) {
\core\component::reset(true);
}
$this->fullcomponentreset = false;
}
/**
* Call this method to force the component cache to be fully reset after each test.
*
* Normally the component cache is reset, but only partially, to speed up the tests.
* This method will cause the reset to also cause re-reads off disk.
*
* @param bool $reset Whether to fully reset the component cache after each test
*/
protected function fully_reset_component_after_test(bool $reset = true): void {
$this->fullcomponentreset = $reset;
}
/**
* Add a mocked plugintype at the component sources level, allowing \core\component to pick up the type and its plugins.
*
* Unlike {@see add_mocked_plugintype}, this method doesn't inject the mocked type into the existing cache at surface level,
* but instead injects it into component sources, allowing \core\component to fully populate its caches from the mock sources.
*
* Please note that tests calling this method must be run in separate isolation mode.
* Please avoid using this if at all possible.
*
* @param string $plugintype The name of the plugintype
* @param string $path The path to the plugintype's root
* @param bool $subpluginsupport whether the mock plugintype supports subplugins.
* @return void
*/
protected function add_full_mocked_plugintype(
string $plugintype,
string $path,
bool $subpluginsupport = false
): void {
require_phpunit_isolation();
$this->fully_reset_component_after_test(true);
// Inject the plugintype into the mock component sources. This will be picked up during \core\component::init().
$mockedcomponent = new \ReflectionClass(\core\component::class);
$componentsource = $mockedcomponent->getStaticPropertyValue('componentsource');
$componentsourcekey = 'plugintypes';
if (object_property_exists($componentsource[$componentsourcekey], $plugintype)) {
throw new \coding_exception("The plugintype '{$plugintype}' already exists in component sources.");
}
$componentsource[$componentsourcekey]->$plugintype = $path;
$mockedcomponent->setStaticPropertyValue('componentsource', $componentsource);
// Force subplugin support for the plugin type, if specified.
if ($subpluginsupport) {
$typessupporting = $mockedcomponent->getStaticPropertyValue('supportsubplugins');
if (array_search($plugintype, $typessupporting) === false) {
$typessupporting[] = $plugintype;
}
$mockedcomponent->setStaticPropertyValue('supportsubplugins', $typessupporting);
}
// Force clear the static plugintypes cache, as this cache determines whether \core\component::init() will rebuild
// core\component caches from component sources.
$mockedcomponent->setStaticPropertyValue('plugintypes', null);
// Mock the installation of all plugins belonging to the plugintype (and those from the subtypes, if supported).
$allpluginsoftype = \core\component::get_plugin_list($plugintype);
foreach ($allpluginsoftype as $name => $plugindir) {
// Mock the installation of the plugin.
$plugin = (object) [];
require("$plugindir/version.php");
$fullpluginname = $plugintype . '_' . $name;
set_config('version', $plugin->version, $fullpluginname);
update_capabilities($fullpluginname);
// Mock the installation of the subplugins, if supported.
if ($subpluginsupport) {
if ($subpluginsoftype = \core\component::get_all_subplugins($fullpluginname)) {
$alltypes = \core\component::get_all_plugin_types();
foreach ($subpluginsoftype as $subplugintype => $subplugins) {
foreach ($subplugins as $index => $name) {
$subplugindir = $alltypes[$subplugintype] . '/' . $name;
$fullsubpluginname = $subplugintype . '_' . $name;
$plugin = (object) [];
require("$subplugindir/version.php");
set_config('version', $plugin->version, $fullsubpluginname);
update_capabilities($fullsubpluginname);
}
}
}
}
}
// Finally purge whatever was already cached in plugin_manager.
\cache::make('core', 'plugin_manager')->purge();
}
/**
* Helper to deprecate a mocked plugin type at the component sources level.
*
* This method is to be used alongside {@see add_full_mocked_plugintype} only. It does not support deprecating shallow mocks
* of plugin types, such as those created with {@see add_mocked_plugintype}.
*
* @param string $plugintype the plugin type.
* @return void
* @throws coding_exception if the plugintype hasn't already been mocked or if it's already been deprecated.
*/
protected function deprecate_full_mocked_plugintype(
string $plugintype,
): void {
$this->fully_reset_component_after_test(true);
$mockedcomponent = new \ReflectionClass(\core\component::class);
$componentsource = $mockedcomponent->getStaticPropertyValue('componentsource');
$deprecatedkey = 'deprecatedplugintypes';
$typeskey = 'plugintypes';
$componentsource['deprecatedplugintypes'] = $componentsource['deprecatedplugintypes'] ?? (object) [];
if (!object_property_exists($componentsource[$typeskey], $plugintype)) {
throw new coding_exception("The plugintype '{$plugintype}' does not exist and cannot be deprecated.");
}
if (object_property_exists($componentsource[$deprecatedkey], $plugintype)) {
throw new coding_exception("The plugintype '{$plugintype}' has already been deprecated.");
}
$componentsource[$deprecatedkey]->$plugintype = $componentsource[$typeskey]->$plugintype;
unset($componentsource[$typeskey]->$plugintype);
$mockedcomponent->setStaticPropertyValue('componentsource', $componentsource);
// Force clear the static plugintypes cache, as this cache determines whether \core\component::init() will rebuild
// \core\component caches from component sources.
$mockedcomponent->setStaticPropertyValue('plugintypes', null);
}
/**
* Helper to delete a mocked plugin type at the component sources level.
*
* This method is to be used alongside {@see add_full_mocked_plugintype} only. It does not support deleting shallow mocks
* of plugin types, such as those created with {@see add_mocked_plugintype}.
*
* @param string $plugintype the plugin type.
* @return void
* @throws coding_exception if the plugintype hasn't already been mocked or if it's already been deprecated.
*/
protected function delete_full_mocked_plugintype(
string $plugintype,
): void {
$this->fully_reset_component_after_test(true);
\core\component::classloader(\core\component::class);
$mockedcomponent = new \ReflectionClass(\core\component::class);
$componentsource = $mockedcomponent->getStaticPropertyValue('componentsource');
$deletedkey = 'deletedplugintypes';
$typeskey = 'plugintypes';
$componentsource['deletedplugintypes'] = $componentsource['deletedplugintypes'] ?? (object) [];
if (!object_property_exists($componentsource[$typeskey], $plugintype)) {
throw new coding_exception("The plugintype '{$plugintype}' does not exist and cannot be deleted.");
}
if (object_property_exists($componentsource[$deletedkey], $plugintype)) {
throw new coding_exception("The plugintype '{$plugintype}' has already been deleted.");
}
$componentsource[$deletedkey]->$plugintype = $componentsource[$typeskey]->$plugintype;
unset($componentsource[$typeskey]->$plugintype);
$mockedcomponent->setStaticPropertyValue('componentsource', $componentsource);
// Force clear the static plugintypes cache, as this cache determines whether \core\component::init() will rebuild
// core\component caches from component sources.
$mockedcomponent->setStaticPropertyValue('plugintypes', null);
}
}