Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace core\tests;
18
 
19
/**
20
 * Trait support full/deep plugin/subplugin mocking, forcing \core\component to rebuild in full.
21
 *
22
 * Useful for testing core\component, plugin managers, and lower level classes of that nature.
23
 *
24
 * For other plugin mocking, shallow mocking may be more suitable. See:
25
 * {@see \advanced_testcase::add_mocked_plugintype}
26
 * {@see \advanced_testcase::add_mocked_plugin()}
27
 *
28
 * @package    core
29
 * @category   phpunit
30
 * @copyright  2025 Jake Dallimore <jrhdallimore@gmail.com>
31
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32
 */
33
trait fake_plugins_test_trait {
34
    /** @var bool Whether to fully reset the component cache after each test */
35
    protected bool $fullcomponentreset = false;
36
 
37
    /**
38
     * Reset the component cache.
39
     *
40
     * This is an After test method that supplements the test tearDown to ensure the component cache is reset.
41
     *
42
     * Please note that the component cache is always partially reset in the main tearDown method.
43
     * This method will extend the reset to also cause re-reads off disk.
44
     */
45
    #[\PHPUnit\Framework\Attributes\After]
46
    public function reset_component_cache(): void {
47
        if ($this->fullcomponentreset === true) {
48
            \core\component::reset(true);
49
        }
50
        $this->fullcomponentreset = false;
51
    }
52
 
53
    /**
54
     * Call this method to force the component cache to be fully reset after each test.
55
     *
56
     * Normally the component cache is reset, but only partially, to speed up the tests.
57
     * This method will cause the reset to also cause re-reads off disk.
58
     *
59
     * @param bool $reset Whether to fully reset the component cache after each test
60
     */
61
    protected function fully_reset_component_after_test(bool $reset = true): void {
62
        $this->fullcomponentreset = $reset;
63
    }
64
 
65
    /**
66
     * Add a mocked plugintype at the component sources level, allowing \core\component to pick up the type and its plugins.
67
     *
68
     * Unlike {@see add_mocked_plugintype}, this method doesn't inject the mocked type into the existing cache at surface level,
69
     * but instead injects it into component sources, allowing \core\component to fully populate its caches from the mock sources.
70
     *
71
     * Please note that tests calling this method must be run in separate isolation mode.
72
     * Please avoid using this if at all possible.
73
     *
74
     * @param string $plugintype The name of the plugintype
75
     * @param string $path The path to the plugintype's root
76
     * @param bool $subpluginsupport whether the mock plugintype supports subplugins.
77
     * @return void
78
     */
79
    protected function add_full_mocked_plugintype(
80
        string $plugintype,
81
        string $path,
82
        bool $subpluginsupport = false
83
    ): void {
84
        require_phpunit_isolation();
85
        $this->fully_reset_component_after_test(true);
86
 
87
        // Inject the plugintype into the mock component sources. This will be picked up during \core\component::init().
88
        $mockedcomponent = new \ReflectionClass(\core\component::class);
89
        $componentsource = $mockedcomponent->getStaticPropertyValue('componentsource');
90
        $componentsourcekey = 'plugintypes';
91
        if (object_property_exists($componentsource[$componentsourcekey], $plugintype)) {
92
            throw new \coding_exception("The plugintype '{$plugintype}' already exists in component sources.");
93
        }
94
        $componentsource[$componentsourcekey]->$plugintype = $path;
95
        $mockedcomponent->setStaticPropertyValue('componentsource', $componentsource);
96
 
97
        // Force subplugin support for the plugin type, if specified.
98
        if ($subpluginsupport) {
99
            $typessupporting = $mockedcomponent->getStaticPropertyValue('supportsubplugins');
100
            if (array_search($plugintype, $typessupporting) === false) {
101
                $typessupporting[] = $plugintype;
102
            }
103
            $mockedcomponent->setStaticPropertyValue('supportsubplugins', $typessupporting);
104
        }
105
 
106
        // Force clear the static plugintypes cache, as this cache determines whether \core\component::init() will rebuild
107
        // core\component caches from component sources.
108
        $mockedcomponent->setStaticPropertyValue('plugintypes', null);
109
 
110
        // Mock the installation of all plugins belonging to the plugintype (and those from the subtypes, if supported).
111
        $allpluginsoftype = \core\component::get_plugin_list($plugintype);
112
        foreach ($allpluginsoftype as $name => $plugindir) {
113
            // Mock the installation of the plugin.
114
            $plugin = (object) [];
115
            require("$plugindir/version.php");
116
            $fullpluginname = $plugintype . '_' . $name;
117
            set_config('version', $plugin->version, $fullpluginname);
118
            update_capabilities($fullpluginname);
119
 
120
            // Mock the installation of the subplugins, if supported.
121
            if ($subpluginsupport) {
122
                if ($subpluginsoftype = \core\component::get_all_subplugins($fullpluginname)) {
123
                    $alltypes = \core\component::get_all_plugin_types();
124
                    foreach ($subpluginsoftype as $subplugintype => $subplugins) {
125
                        foreach ($subplugins as $index => $name) {
126
                            $subplugindir = $alltypes[$subplugintype] . '/' . $name;
127
                            $fullsubpluginname = $subplugintype . '_' . $name;
128
                            $plugin = (object) [];
129
                            require("$subplugindir/version.php");
130
                            set_config('version', $plugin->version, $fullsubpluginname);
131
                            update_capabilities($fullsubpluginname);
132
                        }
133
                    }
134
                }
135
            }
136
        }
137
 
138
        // Finally purge whatever was already cached in plugin_manager.
139
        \cache::make('core', 'plugin_manager')->purge();
140
    }
141
 
142
    /**
143
     * Helper to deprecate a mocked plugin type at the component sources level.
144
     *
145
     * This method is to be used alongside {@see add_full_mocked_plugintype} only. It does not support deprecating shallow mocks
146
     * of plugin types, such as those created with {@see add_mocked_plugintype}.
147
     *
148
     * @param string $plugintype the plugin type.
149
     * @return void
150
     * @throws coding_exception if the plugintype hasn't already been mocked or if it's already been deprecated.
151
     */
152
    protected function deprecate_full_mocked_plugintype(
153
        string $plugintype,
154
    ): void {
155
        $this->fully_reset_component_after_test(true);
156
 
157
        $mockedcomponent = new \ReflectionClass(\core\component::class);
158
        $componentsource = $mockedcomponent->getStaticPropertyValue('componentsource');
159
        $deprecatedkey = 'deprecatedplugintypes';
160
        $typeskey = 'plugintypes';
161
        $componentsource['deprecatedplugintypes'] = $componentsource['deprecatedplugintypes'] ?? (object) [];
162
        if (!object_property_exists($componentsource[$typeskey], $plugintype)) {
163
            throw new coding_exception("The plugintype '{$plugintype}' does not exist and cannot be deprecated.");
164
        }
165
        if (object_property_exists($componentsource[$deprecatedkey], $plugintype)) {
166
            throw new coding_exception("The plugintype '{$plugintype}' has already been deprecated.");
167
        }
168
        $componentsource[$deprecatedkey]->$plugintype = $componentsource[$typeskey]->$plugintype;
169
        unset($componentsource[$typeskey]->$plugintype);
170
        $mockedcomponent->setStaticPropertyValue('componentsource', $componentsource);
171
 
172
        // Force clear the static plugintypes cache, as this cache determines whether \core\component::init() will rebuild
173
        // \core\component caches from component sources.
174
        $mockedcomponent->setStaticPropertyValue('plugintypes', null);
175
    }
176
 
177
    /**
178
     * Helper to delete a mocked plugin type at the component sources level.
179
     *
180
     * This method is to be used alongside {@see add_full_mocked_plugintype} only. It does not support deleting shallow mocks
181
     * of plugin types, such as those created with {@see add_mocked_plugintype}.
182
     *
183
     * @param string $plugintype the plugin type.
184
     * @return void
185
     * @throws coding_exception if the plugintype hasn't already been mocked or if it's already been deprecated.
186
     */
187
    protected function delete_full_mocked_plugintype(
188
        string $plugintype,
189
    ): void {
190
        $this->fully_reset_component_after_test(true);
191
 
192
        \core\component::classloader(\core\component::class);
193
        $mockedcomponent = new \ReflectionClass(\core\component::class);
194
        $componentsource = $mockedcomponent->getStaticPropertyValue('componentsource');
195
        $deletedkey = 'deletedplugintypes';
196
        $typeskey = 'plugintypes';
197
        $componentsource['deletedplugintypes'] = $componentsource['deletedplugintypes'] ?? (object) [];
198
        if (!object_property_exists($componentsource[$typeskey], $plugintype)) {
199
            throw new coding_exception("The plugintype '{$plugintype}' does not exist and cannot be deleted.");
200
        }
201
        if (object_property_exists($componentsource[$deletedkey], $plugintype)) {
202
            throw new coding_exception("The plugintype '{$plugintype}' has already been deleted.");
203
        }
204
        $componentsource[$deletedkey]->$plugintype = $componentsource[$typeskey]->$plugintype;
205
        unset($componentsource[$typeskey]->$plugintype);
206
        $mockedcomponent->setStaticPropertyValue('componentsource', $componentsource);
207
 
208
        // Force clear the static plugintypes cache, as this cache determines whether \core\component::init() will rebuild
209
        // core\component caches from component sources.
210
        $mockedcomponent->setStaticPropertyValue('plugintypes', null);
211
    }
212
}