Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
16
 
17
namespace core\hook;
18
 
19
use core\di;
20
 
21
/**
22
 * Hooks tests.
23
 *
24
 * @package   core
25
 * @author    Petr Skoda
26
 * @copyright 2022 Open LMS
27
 * @license   https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28
 * @covers \core\hook\manager
29
 */
30
final class manager_test extends \advanced_testcase {
31
    /**
32
     * Test public factory method to get hook manager.
33
     */
34
    public function test_get_instance(): void {
35
        $manager = manager::get_instance();
36
        $this->assertInstanceOf(manager::class, $manager);
37
 
38
        $this->assertSame($manager, manager::get_instance());
39
    }
40
 
41
    /**
42
     * Test getting of manager test instance.
43
     */
44
    public function test_phpunit_get_instance(): void {
45
        $testmanager = manager::phpunit_get_instance([]);
46
        $this->assertSame([], $testmanager->get_hooks_with_callbacks());
47
 
48
        // We get a new instance every time.
49
        $this->assertNotSame($testmanager, manager::phpunit_get_instance([]));
50
 
51
        $componentfiles = [
52
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
53
        ];
54
        $testmanager = manager::phpunit_get_instance($componentfiles);
55
        $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
56
    }
57
 
58
    /**
59
     * Test loading and parsing of callbacks from files.
60
     */
61
    public function test_callbacks(): void {
62
        $componentfiles = [
63
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
64
            'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
65
        ];
66
        $testmanager = manager::phpunit_get_instance($componentfiles);
67
        $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
68
        $callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
69
        $this->assertCount(2, $callbacks);
70
        $this->assertSame([
71
            'callback' => 'test_plugin\\callbacks::test2',
72
            'component' => 'test_plugin2',
73
            'disabled' => false,
74
            'priority' => 200,
75
        ], $callbacks[0]);
76
        $this->assertSame([
77
            'callback' => 'test_plugin\\callbacks::test1',
78
            'component' => 'test_plugin1',
79
            'disabled' => false,
80
            'priority' => 100,
81
        ], $callbacks[1]);
82
 
83
        $this->assertDebuggingNotCalled();
84
        $componentfiles = [
85
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_broken.php',
86
        ];
87
        $testmanager = manager::phpunit_get_instance($componentfiles);
88
        $this->assertSame([], $testmanager->get_hooks_with_callbacks());
89
        $debuggings = $this->getDebuggingMessages();
90
        $this->resetDebugging();
91
        $this->assertSame(
92
            'Hook callback definition requires \'hook\' name in \'test_plugin1\'',
93
            $debuggings[0]->message
94
        );
95
        $this->assertSame(
96
            'Hook callback definition requires \'callback\' callable in \'test_plugin1\'',
97
            $debuggings[1]->message
98
        );
99
        $this->assertSame(
100
            'Hook callback definition contains invalid \'callback\' static class method string in \'test_plugin1\'',
101
            $debuggings[2]->message
102
        );
103
        $this->assertCount(3, $debuggings);
104
    }
105
 
106
    /**
107
     * Test hook dispatching, that is callback execution.
108
     */
109
    public function test_dispatch(): void {
110
        require_once(__DIR__ . '/../fixtures/hook/hook.php');
111
        require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
112
 
113
        $componentfiles = [
114
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
115
            'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
116
        ];
117
        $testmanager = manager::phpunit_get_instance($componentfiles);
118
        \test_plugin\callbacks::$calls = [];
119
        $hook = new \test_plugin\hook\hook();
120
        $result = $testmanager->dispatch($hook);
121
        $this->assertSame($hook, $result);
122
        $this->assertSame(['test2', 'test1'], \test_plugin\callbacks::$calls);
123
        \test_plugin\callbacks::$calls = [];
124
        $this->assertDebuggingNotCalled();
125
    }
126
 
127
    /**
128
     * Test hook dispatching, that is callback execution.
129
     */
130
    public function test_dispatch_with_exception(): void {
131
        require_once(__DIR__ . '/../fixtures/hook/hook.php');
132
        require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
133
 
134
        $componentfiles = [
135
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_exception.php',
136
            'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
137
        ];
138
        $testmanager = manager::phpunit_get_instance($componentfiles);
139
 
140
        $hook = new \test_plugin\hook\hook();
141
 
142
        $this->expectException(\Exception::class);
143
        $this->expectExceptionMessage('grrr');
144
 
145
        $testmanager->dispatch($hook);
146
    }
147
 
148
    /**
149
     * Test hook dispatching, that is callback execution.
150
     */
151
    public function test_dispatch_with_invalid(): void {
152
        require_once(__DIR__ . '/../fixtures/hook/hook.php');
153
        require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
154
 
155
        // Missing callbacks is ignored.
156
        $componentfiles = [
157
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_missing.php',
158
            'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
159
        ];
160
        $testmanager = manager::phpunit_get_instance($componentfiles);
161
        \test_plugin\callbacks::$calls = [];
162
 
163
        $hook = new \test_plugin\hook\hook();
164
 
165
        $testmanager->dispatch($hook);
166
        $this->assertDebuggingCalled(
167
            "Hook callback definition contains invalid 'callback' method name in 'test_plugin1'. Callback method not found.",
168
        );
169
        $this->assertSame(['test2'], \test_plugin\callbacks::$calls);
170
    }
171
 
172
    /**
173
     * Test stoppping of hook dispatching.
174
     */
175
    public function test_dispatch_stoppable(): void {
176
        require_once(__DIR__ . '/../fixtures/hook/stoppablehook.php');
177
        require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
178
 
179
        $componentfiles = [
180
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_stoppable.php',
181
            'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_stoppable.php',
182
        ];
183
        $testmanager = manager::phpunit_get_instance($componentfiles);
184
        \test_plugin\callbacks::$calls = [];
185
        $hook = new \test_plugin\hook\stoppablehook();
186
        $result = $testmanager->dispatch($hook);
187
        $this->assertSame($hook, $result);
188
        $this->assertSame(['stop1'], \test_plugin\callbacks::$calls);
189
        \test_plugin\callbacks::$calls = [];
190
        $this->assertDebuggingNotCalled();
191
    }
192
 
193
    /**
194
     * Tests callbacks can be overridden via CFG settings.
195
     */
196
    public function test_callback_overriding(): void {
197
        global $CFG;
198
        $this->resetAfterTest();
199
 
200
        $componentfiles = [
201
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
202
            'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
203
        ];
204
 
205
        $testmanager = manager::phpunit_get_instance($componentfiles);
206
        $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
207
        $callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
208
        $this->assertCount(2, $callbacks);
209
        $this->assertSame([
210
            'callback' => 'test_plugin\\callbacks::test2',
211
            'component' => 'test_plugin2',
212
            'disabled' => false,
213
            'priority' => 200,
214
        ], $callbacks[0]);
215
        $this->assertSame([
216
            'callback' => 'test_plugin\\callbacks::test1',
217
            'component' => 'test_plugin1',
218
            'disabled' => false,
219
            'priority' => 100,
220
        ], $callbacks[1]);
221
 
222
        $CFG->hooks_callback_overrides = [
223
            'test_plugin\\hook\\hook' => [
224
                'test_plugin\\callbacks::test2' => ['priority' => 33],
225
            ],
226
        ];
227
 
228
        $testmanager = manager::phpunit_get_instance($componentfiles);
229
        $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
230
        $callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
231
        $this->assertCount(2, $callbacks);
232
        $this->normalise_callbacks($callbacks);
233
        $this->assertSame([
234
            'callback' => 'test_plugin\\callbacks::test1',
235
            'component' => 'test_plugin1',
236
            'disabled' => false,
237
            'priority' => 100,
238
        ], $callbacks[0]);
239
        $this->assertSame([
240
            'callback' => 'test_plugin\\callbacks::test2',
241
            'component' => 'test_plugin2',
242
            'defaultpriority' => 200,
243
            'disabled' => false,
244
            'priority' => 33,
245
        ], $callbacks[1]);
246
 
247
        $CFG->hooks_callback_overrides = [
248
            'test_plugin\\hook\\hook' => [
249
                'test_plugin\\callbacks::test2' => ['priority' => 33, 'disabled' => true],
250
            ],
251
        ];
252
        $testmanager = manager::phpunit_get_instance($componentfiles);
253
        $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
254
        $callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
255
        $this->assertCount(2, $callbacks);
256
        $this->normalise_callbacks($callbacks);
257
        $this->assertSame([
258
            'callback' => 'test_plugin\\callbacks::test1',
259
            'component' => 'test_plugin1',
260
            'disabled' => false,
261
            'priority' => 100,
262
        ], $callbacks[0]);
263
        $this->assertSame([
264
            'callback' => 'test_plugin\\callbacks::test2',
265
            'component' => 'test_plugin2',
266
            'defaultpriority' => 200,
267
            'disabled' => true,
268
            'priority' => 33,
269
        ], $callbacks[1]);
270
 
271
        $CFG->hooks_callback_overrides = [
272
            'test_plugin\\hook\\hook' => [
273
                'test_plugin\\callbacks::test2' => ['disabled' => true],
274
            ],
275
        ];
276
        $testmanager = manager::phpunit_get_instance($componentfiles);
277
        $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
278
        $callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
279
        $this->assertCount(2, $callbacks);
280
        $this->assertSame([
281
            'callback' => 'test_plugin\\callbacks::test2',
282
            'component' => 'test_plugin2',
283
            'disabled' => true,
284
            'priority' => 200,
285
        ], $callbacks[0]);
286
        $this->assertSame([
287
            'callback' => 'test_plugin\\callbacks::test1',
288
            'component' => 'test_plugin1',
289
            'disabled' => false,
290
            'priority' => 100,
291
        ], $callbacks[1]);
292
 
293
        require_once(__DIR__ . '/../fixtures/hook/hook.php');
294
        require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
295
 
296
        \test_plugin\callbacks::$calls = [];
297
        $hook = new \test_plugin\hook\hook();
298
        $result = $testmanager->dispatch($hook);
299
        $this->assertSame($hook, $result);
300
        $this->assertSame(['test1'], \test_plugin\callbacks::$calls);
301
        \test_plugin\callbacks::$calls = [];
302
        $this->assertDebuggingNotCalled();
303
        $CFG->hooks_callback_overrides = [];
304
    }
305
 
306
    /**
307
     * Register a fake plugin called hooktest in the component manager.
308
     *
309
     * Tests consuming this helpers must run in a separate process.
310
     */
311
    protected function setup_hooktest_plugin(): void {
312
        global $CFG;
313
 
314
        $this->add_mocked_plugintype('fake', "{$CFG->dirroot}/lib/tests/fixtures/hook/fakeplugins");
315
        $this->add_mocked_plugin('fake', 'hooktest', "{$CFG->dirroot}/lib/tests/fixtures/hook/fakeplugins/hooktest");
316
    }
317
 
318
    /**
319
     * Call a plugin callback that has been replaced by a hook, but has no hook callback.
320
     *
321
     * The original callback should be called, but a debugging message should be output.
322
     *
323
     * @runInSeparateProcess
324
     */
325
    public function test_migrated_callback(): void {
326
        $this->resetAfterTest(true);
327
        // Include plugin hook discovery agent, and the hook that replaces the callback.
328
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php');
329
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_callback.php');
330
        // Register the fake plugin with the component manager.
331
        $this->setup_hooktest_plugin();
332
 
333
        // Register the fake plugin with the hook manager, but don't define any hook callbacks.
334
        di::set(
335
            manager::class,
336
            manager::phpunit_get_instance(
337
                [
338
                    'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks_nocallbacks.php',
339
                ],
340
            ),
341
        );
342
 
343
        // Confirm a non-deprecated callback is called as expected.
344
        $this->assertEquals('Called current callback', component_callback('fake_hooktest', 'current_callback'));
345
 
346
        // Confirm the deprecated callback is called as expected.
347
        $this->assertEquals(
348
            'Called deprecated callback',
349
            component_callback('fake_hooktest', 'old_callback', [], null, true)
350
        );
351
        $this->assertDebuggingCalled(
352
            'Callback old_callback in fake_hooktest component should be migrated to new hook ' .
353
                'callback for fake_hooktest\hook\hook_replacing_callback'
354
        );
355
    }
356
 
357
    /**
358
     * Call a plugin callback that has been replaced by a hook, and has a hook callback.
359
     *
360
     * The original callback should not be called, and no debugging should be output.
361
     *
362
     * @runInSeparateProcess
363
     */
364
    public function test_migrated_callback_with_replacement(): void {
365
        $this->resetAfterTest(true);
366
        // Include plugin hook discovery agent, and the hook that replaces the callback, and a hook callback for the hook.
367
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php');
368
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_callback.php');
369
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook_callbacks.php');
370
        // Register the fake plugin with the component manager.
371
        $this->setup_hooktest_plugin();
372
 
373
        // Register the fake plugin with the hook manager, including the hook callback.
374
        di::set(
375
            manager::class,
376
            manager::phpunit_get_instance([
377
                'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks.php',
378
            ]),
379
        );
380
 
381
        // Confirm a non-deprecated callback is called as expected.
382
        $this->assertEquals('Called current callback', component_callback('fake_hooktest', 'current_callback'));
383
 
384
        // Confirm the deprecated callback is not called, as expected.
385
        $this->assertNull(component_callback('fake_hooktest', 'old_callback', [], null, true));
386
        $this->assertDebuggingNotCalled();
387
    }
388
 
389
    /**
390
     * Call a plugin class callback that has been replaced by a hook, but has no hook callback.
391
     *
392
     * The original class callback should be called, but a debugging message should be output.
393
     *
394
     * @runInSeparateProcess
395
     */
396
    public function test_migrated_class_callback(): void {
397
        $this->resetAfterTest(true);
398
        // Include plugin hook discovery agent, the class containing callbacks, and the hook that replaces the class callback.
399
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/callbacks.php');
400
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php');
401
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_class_callback.php');
402
        // Register the fake plugin with the component manager.
403
        $this->setup_hooktest_plugin();
404
 
405
        // Register the fake plugin with the hook manager, but don't define any hook callbacks.
406
        di::set(
407
            manager::class,
408
            manager::phpunit_get_instance([
409
                'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks_nocallbacks.php',
410
            ]),
411
        );
412
 
413
        // Confirm a non-deprecated class callback is called as expected.
414
        $this->assertEquals(
415
            'Called current class callback',
416
            component_class_callback('fake_hooktest\callbacks', 'current_class_callback', [])
417
        );
418
 
419
        // Confirm the deprecated class callback is called as expected.
420
        $this->assertEquals(
421
            'Called deprecated class callback',
422
            component_class_callback('fake_hooktest\callbacks', 'old_class_callback', [], null, true)
423
        );
424
        $this->assertDebuggingCalled(
425
            'Callback callbacks::old_class_callback in fake_hooktest component should be migrated to new hook ' .
426
                'callback for fake_hooktest\hook\hook_replacing_class_callback'
427
        );
428
    }
429
 
430
    /**
431
     * Call a plugin class callback that has been replaced by a hook, and has a hook callback.
432
     *
433
     * The original callback should not be called, and no debugging should be output.
434
     *
435
     * @runInSeparateProcess
436
     */
437
    public function test_migrated_class_callback_with_replacement(): void {
438
        $this->resetAfterTest(true);
439
        // Include plugin hook discovery agent, the class containing callbacks, the hook that replaces the class callback,
440
        // and a hook callback for the new hook.
441
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/callbacks.php');
442
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php');
443
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_class_callback.php');
444
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook_callbacks.php');
445
        // Register the fake plugin with the component manager.
446
        $this->setup_hooktest_plugin();
447
 
448
        // Register the fake plugin with the hook manager, including the hook callback.
449
        di::set(
450
            manager::class,
451
            manager::phpunit_get_instance([
452
                'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks.php',
453
            ]),
454
        );
455
 
456
        // Confirm a non-deprecated class callback is called as expected.
457
        $this->assertEquals(
458
            'Called current class callback',
459
            component_class_callback('fake_hooktest\callbacks', 'current_class_callback', [])
460
        );
461
 
462
        // Confirm the deprecated class callback is not called, as expected.
463
        $this->assertNull(component_class_callback('fake_hooktest\callbacks', 'old_class_callback', [], null, true));
464
        $this->assertDebuggingNotCalled();
465
    }
466
 
467
    /**
468
     * Normalise the sort order of callbacks to help with asserts.
469
     *
470
     * @param array $callbacks
471
     */
472
    private function normalise_callbacks(array &$callbacks): void {
473
        foreach ($callbacks as &$callback) {
474
            ksort($callback);
475
        }
476
    }
477
}