Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 
1441 ariadna 17
namespace core;
1 efrain 18
 
1441 ariadna 19
use core\exception\coding_exception;
20
use core\tests\fake_plugins_test_trait;
21
use DirectoryIterator;
22
use ReflectionClass;
23
use ReflectionProperty;
24
 
1 efrain 25
/**
1441 ariadna 26
 * component related tests.
1 efrain 27
 *
28
 * @package    core
29
 * @category   test
30
 * @copyright  2013 Petr Skoda {@link http://skodak.org}
31
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32
 *
1441 ariadna 33
 * @covers \core\component
1 efrain 34
 */
1441 ariadna 35
final class component_test extends \advanced_testcase {
36
    use fake_plugins_test_trait;
1 efrain 37
 
11 efrain 38
    #[\Override]
39
    public function tearDown(): void {
40
        parent::tearDown();
41
 
1441 ariadna 42
        ini_set('error_log', null);
11 efrain 43
    }
44
 
1441 ariadna 45
    /**
46
     * To be changed if number of subsystems increases/decreases,
47
     * this is defined here to annoy devs that try to add more without any thinking,
48
     * always verify that it does not collide with any existing add-on modules and subplugins!!!
49
     */
50
    const SUBSYSTEMCOUNT = 80;
51
 
1 efrain 52
    public function test_get_core_subsystems(): void {
53
        global $CFG;
54
 
1441 ariadna 55
        $subsystems = component::get_core_subsystems();
1 efrain 56
 
57
        $this->assertCount(
58
            self::SUBSYSTEMCOUNT,
59
            $subsystems,
60
            'Oh, somebody added or removed a core subsystem, think twice before doing that!',
61
        );
62
 
63
        // Make sure all paths are full/null, exist and are inside dirroot.
64
        foreach ($subsystems as $subsystem => $fulldir) {
65
            $this->assertFalse(strpos($subsystem, '_'), 'Core subsystems must be one work without underscores');
66
            if ($fulldir === null) {
67
                if ($subsystem === 'filepicker' || $subsystem === 'help') { // phpcs:ignore
68
                    // Arrgghh, let's not introduce more subsystems for no real reason...
69
                } else {
70
                    // Lang strings.
71
                    $this->assertFileExists(
72
                        "$CFG->dirroot/lang/en/$subsystem.php",
73
                        'Core subsystems without fulldir are usually used for lang strings.',
74
                    );
75
                }
76
                continue;
77
            }
78
            $this->assertFileExists($fulldir);
79
            // Check that base uses realpath() separators and "/" in the subdirs.
80
            $this->assertStringStartsWith($CFG->dirroot . '/', $fulldir);
81
            $reldir = substr($fulldir, strlen($CFG->dirroot) + 1);
82
            $this->assertFalse(strpos($reldir, '\\'));
83
        }
84
 
85
        // Make sure all core language files are also subsystems!
86
        $items = new DirectoryIterator("$CFG->dirroot/lang/en");
87
        foreach ($items as $item) {
88
            if ($item->isDot() || $item->isDir()) {
89
                continue;
90
            }
91
            $file = $item->getFilename();
92
            if ($file === 'moodle.php') {
93
                // Do not add new lang strings unless really necessary!!!
94
                continue;
95
            }
96
 
97
            if (substr($file, -4) !== '.php') {
98
                continue;
99
            }
100
            $file = substr($file, 0, strlen($file) - 4);
101
            $this->assertArrayHasKey(
102
                $file,
103
                $subsystems,
104
                'All core lang files should be subsystems, think twice before adding anything!',
105
            );
106
        }
107
        unset($item);
108
        unset($items);
109
    }
110
 
111
    public function test_deprecated_get_core_subsystems(): void {
112
        global $CFG;
113
 
1441 ariadna 114
        $subsystems = component::get_core_subsystems();
1 efrain 115
 
116
        $this->assertSame($subsystems, get_core_subsystems(true));
1441 ariadna 117
        $this->assertDebuggingCalled();
118
        $this->resetDebugging();
1 efrain 119
 
120
        $realsubsystems = get_core_subsystems();
1441 ariadna 121
        $this->assertdebuggingcalledcount(2);
122
        $this->resetDebugging();
123
 
1 efrain 124
        $this->assertSame($realsubsystems, get_core_subsystems(false));
1441 ariadna 125
        $this->assertdebuggingcalledcount(2);
126
        $this->resetDebugging();
1 efrain 127
 
128
        $this->assertEquals(count($subsystems), count($realsubsystems));
129
 
130
        foreach ($subsystems as $subsystem => $fulldir) {
131
            $this->assertArrayHasKey($subsystem, $realsubsystems);
132
            if ($fulldir === null) {
133
                $this->assertNull($realsubsystems[$subsystem]);
134
                continue;
135
            }
136
            $this->assertSame($fulldir, $CFG->dirroot . '/' . $realsubsystems[$subsystem]);
137
        }
138
    }
139
 
140
    public function test_get_plugin_types(): void {
141
        global $CFG;
142
 
143
        $this->assertTrue(
144
            empty($CFG->themedir),
145
            'Non-empty $CFG->themedir is not covered by any tests yet, you need to disable it.',
146
        );
147
 
1441 ariadna 148
        $plugintypes = component::get_plugin_types();
1 efrain 149
 
150
        foreach ($plugintypes as $plugintype => $fulldir) {
151
            $this->assertStringStartsWith("$CFG->dirroot/", $fulldir);
152
        }
153
    }
154
 
155
    public function test_deprecated_get_plugin_types(): void {
156
        global $CFG;
157
 
1441 ariadna 158
        $plugintypes = component::get_plugin_types();
1 efrain 159
 
160
        $this->assertSame($plugintypes, get_plugin_types());
1441 ariadna 161
        $this->assertDebuggingCalled();
162
        $this->resetDebugging();
163
 
1 efrain 164
        $this->assertSame($plugintypes, get_plugin_types(true));
1441 ariadna 165
        $this->assertDebuggingCalled();
166
        $this->resetDebugging();
1 efrain 167
 
168
        $realplugintypes = get_plugin_types(false);
1441 ariadna 169
        $this->assertdebuggingcalledcount(2);
170
        $this->resetDebugging();
1 efrain 171
 
172
        foreach ($plugintypes as $plugintype => $fulldir) {
173
            $this->assertSame($fulldir, $CFG->dirroot . '/' . $realplugintypes[$plugintype]);
174
        }
175
    }
176
 
177
    public function test_get_plugin_list(): void {
178
        global $CFG;
179
 
1441 ariadna 180
        $plugintypes = component::get_plugin_types();
1 efrain 181
 
182
        foreach ($plugintypes as $plugintype => $fulldir) {
1441 ariadna 183
            $plugins = component::get_plugin_list($plugintype);
1 efrain 184
            foreach ($plugins as $pluginname => $plugindir) {
185
                $this->assertStringStartsWith("$CFG->dirroot/", $plugindir);
186
            }
187
            if ($plugintype !== 'auth') {
188
                // Let's crosscheck it with independent implementation (auth/db is an exception).
189
                $reldir = substr($fulldir, strlen($CFG->dirroot) + 1);
190
                $dirs = get_list_of_plugins($reldir);
191
                $dirs = array_values($dirs);
192
                $this->assertDebuggingCalled();
193
                $this->assertSame($dirs, array_keys($plugins));
194
            }
195
        }
196
    }
197
 
198
    public function test_deprecated_get_plugin_list(): void {
1441 ariadna 199
        $plugintypes = component::get_plugin_types();
1 efrain 200
 
201
        foreach ($plugintypes as $plugintype => $fulldir) {
1441 ariadna 202
            $plugins = component::get_plugin_list($plugintype);
1 efrain 203
            $this->assertSame($plugins, get_plugin_list($plugintype));
1441 ariadna 204
            $this->assertDebuggingCalled();
205
            $this->resetDebugging();
1 efrain 206
        }
207
    }
208
 
209
    public function test_get_plugin_directory(): void {
1441 ariadna 210
        $plugintypes = component::get_plugin_types();
1 efrain 211
 
212
        foreach ($plugintypes as $plugintype => $fulldir) {
1441 ariadna 213
            $plugins = component::get_plugin_list($plugintype);
1 efrain 214
            foreach ($plugins as $pluginname => $plugindir) {
1441 ariadna 215
                $this->assertSame($plugindir, component::get_plugin_directory($plugintype, $pluginname));
1 efrain 216
            }
217
        }
218
    }
219
 
220
    public function test_deprecated_get_plugin_directory(): void {
1441 ariadna 221
        $plugintypes = component::get_plugin_types();
1 efrain 222
 
223
        foreach ($plugintypes as $plugintype => $fulldir) {
1441 ariadna 224
            $plugins = component::get_plugin_list($plugintype);
1 efrain 225
            foreach ($plugins as $pluginname => $plugindir) {
226
                $this->assertSame(
1441 ariadna 227
                    component::get_plugin_directory($plugintype, $pluginname),
1 efrain 228
                    get_plugin_directory($plugintype, $pluginname),
229
                );
1441 ariadna 230
                $this->assertDebuggingCalled();
231
                $this->resetDebugging();
1 efrain 232
            }
233
        }
234
    }
235
 
236
    public function test_get_subsystem_directory(): void {
1441 ariadna 237
        $subsystems = component::get_core_subsystems();
1 efrain 238
        foreach ($subsystems as $subsystem => $fulldir) {
1441 ariadna 239
            $this->assertSame($fulldir, component::get_subsystem_directory($subsystem));
1 efrain 240
        }
241
    }
242
 
243
    /**
244
     * Test that the get_plugin_list_with_file() function returns the correct list of plugins.
245
     *
1441 ariadna 246
     * @covers \core\component::is_valid_plugin_name
1 efrain 247
     * @dataProvider is_valid_plugin_name_provider
248
     * @param array $arguments
249
     * @param bool $expected
250
     */
251
    public function test_is_valid_plugin_name(array $arguments, bool $expected): void {
1441 ariadna 252
        $this->assertEquals($expected, component::is_valid_plugin_name(...$arguments));
1 efrain 253
    }
254
 
255
    /**
256
     * Data provider for the is_valid_plugin_name function.
257
     *
258
     * @return array
259
     */
260
    public static function is_valid_plugin_name_provider(): array {
261
        return [
262
            [['mod', 'example1'], true],
263
            [['mod', 'feedback360'], true],
264
            [['mod', 'feedback_360'], false],
265
            [['mod', '2feedback'], false],
266
            [['mod', '1example'], false],
267
            [['mod', 'example.xx'], false],
268
            [['mod', '.example'], false],
269
            [['mod', '_example'], false],
270
            [['mod', 'example_'], false],
271
            [['mod', 'example_x1'], false],
272
            [['mod', 'example-x1'], false],
273
            [['mod', 'role'], false],
274
 
275
            [['tool', 'example1'], true],
276
            [['tool', 'example_x1'], true],
277
            [['tool', 'example_x1_xxx'], true],
278
            [['tool', 'feedback360'], true],
279
            [['tool', 'feed_back360'], true],
280
            [['tool', 'role'], true],
281
            [['tool', '1example'], false],
282
            [['tool', 'example.xx'], false],
283
            [['tool', 'example-xx'], false],
284
            [['tool', '.example'], false],
285
            [['tool', '_example'], false],
286
            [['tool', 'example_'], false],
287
            [['tool', 'example__x1'], false],
288
 
289
            // Some invalid cases.
290
            [['mod', null], false],
291
            [['mod', ''], false],
292
            [['tool', null], false],
293
            [['tool', ''], false],
294
        ];
295
    }
296
 
297
    public function test_normalize_componentname(): void {
298
        // Moodle core.
1441 ariadna 299
        $this->assertSame('core', component::normalize_componentname('core'));
300
        $this->assertSame('core', component::normalize_componentname('moodle'));
301
        $this->assertSame('core', component::normalize_componentname(''));
1 efrain 302
 
303
        // Moodle core subsystems.
1441 ariadna 304
        $this->assertSame('core_admin', component::normalize_componentname('admin'));
305
        $this->assertSame('core_admin', component::normalize_componentname('core_admin'));
306
        $this->assertSame('core_admin', component::normalize_componentname('moodle_admin'));
1 efrain 307
 
308
        // Activity modules and their subplugins.
1441 ariadna 309
        $this->assertSame('mod_workshop', component::normalize_componentname('workshop'));
310
        $this->assertSame('mod_workshop', component::normalize_componentname('mod_workshop'));
311
        $this->assertSame('workshopform_accumulative', component::normalize_componentname('workshopform_accumulative'));
312
        $this->assertSame('mod_quiz', component::normalize_componentname('quiz'));
313
        $this->assertSame('quiz_grading', component::normalize_componentname('quiz_grading'));
314
        $this->assertSame('mod_data', component::normalize_componentname('data'));
315
        $this->assertSame('datafield_checkbox', component::normalize_componentname('datafield_checkbox'));
1 efrain 316
 
317
        // Other plugin types.
1441 ariadna 318
        $this->assertSame('auth_ldap', component::normalize_componentname('auth_ldap'));
319
        $this->assertSame('enrol_self', component::normalize_componentname('enrol_self'));
320
        $this->assertSame('block_html', component::normalize_componentname('block_html'));
321
        $this->assertSame('auth_oauth2', component::normalize_componentname('auth_oauth2'));
322
        $this->assertSame('local_amos', component::normalize_componentname('local_amos'));
323
        $this->assertSame('local_admin', component::normalize_componentname('local_admin'));
1 efrain 324
 
325
        // Unknown words without underscore are supposed to be activity modules.
326
        $this->assertSame(
327
            'mod_whoonearthwouldcomewithsuchastupidnameofcomponent',
1441 ariadna 328
            component::normalize_componentname('whoonearthwouldcomewithsuchastupidnameofcomponent')
1 efrain 329
        );
330
        // Module names can not contain underscores, this must be a subplugin.
331
        $this->assertSame(
332
            'whoonearth_wouldcomewithsuchastupidnameofcomponent',
1441 ariadna 333
            component::normalize_componentname('whoonearth_wouldcomewithsuchastupidnameofcomponent')
1 efrain 334
        );
335
        $this->assertSame(
336
            'whoonearth_would_come_withsuchastupidnameofcomponent',
1441 ariadna 337
            component::normalize_componentname('whoonearth_would_come_withsuchastupidnameofcomponent')
1 efrain 338
        );
339
    }
340
 
1441 ariadna 341
    /**
342
     * Test \core_component::normalize_component function.
343
     *
344
     * @dataProvider normalise_component_provider
345
     * @param array $expected
346
     * @param string $args
347
     */
348
    public function test_normalize_component(array $expected, string $args): void {
1 efrain 349
        $this->assertSame(
1441 ariadna 350
            $expected,
351
            component::normalize_component($args),
1 efrain 352
        );
1441 ariadna 353
    }
354
 
355
    /**
356
     * Test the deprecated normalize_component function.
357
     *
358
     * @dataProvider normalise_component_provider
359
     * @param array $expected
360
     * @param string $args
361
     */
362
    public function test_deprecated_normalize_component(array $expected, string $args): void {
1 efrain 363
        $this->assertSame(
1441 ariadna 364
            $expected,
365
            normalize_component($args),
1 efrain 366
        );
1441 ariadna 367
 
368
        $this->assertDebuggingCalled();
1 efrain 369
    }
370
 
1441 ariadna 371
    /**
372
     * Data provider for the normalize_component function.
373
     */
374
    public static function normalise_component_provider(): array {
375
        return [
376
            // Moodle core.
377
            [['core', null], 'core'],
378
            [['core', null], ''],
379
            [['core', null], 'moodle'],
1 efrain 380
 
1441 ariadna 381
            // Moodle core subsystems.
382
            [['core', 'admin'], 'admin'],
383
            [['core', 'admin'], 'core_admin'],
384
            [['core', 'admin'], 'moodle_admin'],
1 efrain 385
 
1441 ariadna 386
            // Activity modules and their subplugins.
387
            [['mod', 'workshop'], 'workshop'],
388
            [['mod', 'workshop'], 'mod_workshop'],
389
            [['workshopform', 'accumulative'], 'workshopform_accumulative'],
390
            [['mod', 'quiz'], 'quiz'],
391
            [['quiz', 'grading'], 'quiz_grading'],
392
            [['mod', 'data'], 'data'],
393
            [['datafield', 'checkbox'], 'datafield_checkbox'],
1 efrain 394
 
1441 ariadna 395
            // Other plugin types.
396
            [['auth', 'ldap'], 'auth_ldap'],
397
            [['enrol', 'self'], 'enrol_self'],
398
            [['block', 'html'], 'block_html'],
399
            [['auth', 'oauth2'], 'auth_oauth2'],
400
            [['local', 'amos'], 'local_amos'],
401
            [['local', 'admin'], 'local_admin'],
1 efrain 402
 
1441 ariadna 403
            // Unknown words without underscore are supposed to be activity modules.
404
            [
405
                ['mod', 'whoonearthwouldcomewithsuchastupidnameofcomponent'],
406
                'whoonearthwouldcomewithsuchastupidnameofcomponent',
407
            ],
408
            // Module names can not contain underscores, this must be a subplugin.
409
            [
410
                ['whoonearth', 'wouldcomewithsuchastupidnameofcomponent'],
411
                'whoonearth_wouldcomewithsuchastupidnameofcomponent',
412
            ],
413
            [
414
                ['whoonearth', 'would_come_withsuchastupidnameofcomponent'],
415
                'whoonearth_would_come_withsuchastupidnameofcomponent',
416
            ],
417
        ];
1 efrain 418
    }
419
 
420
    public function test_get_component_directory(): void {
1441 ariadna 421
        $plugintypes = component::get_plugin_types();
1 efrain 422
        foreach ($plugintypes as $plugintype => $fulldir) {
1441 ariadna 423
            $plugins = component::get_plugin_list($plugintype);
1 efrain 424
            foreach ($plugins as $pluginname => $plugindir) {
1441 ariadna 425
                $this->assertSame($plugindir, component::get_component_directory(($plugintype . '_' . $pluginname)));
1 efrain 426
            }
427
        }
428
 
1441 ariadna 429
        $subsystems = component::get_core_subsystems();
1 efrain 430
        foreach ($subsystems as $subsystem => $fulldir) {
1441 ariadna 431
            $this->assertSame($fulldir, component::get_component_directory(('core_' . $subsystem)));
1 efrain 432
        }
433
    }
434
 
435
    /**
436
     * Unit tests for get_component_from_classname.
437
     *
438
     * @dataProvider get_component_from_classname_provider
439
     * @param string $classname The class name to test
440
     * @param string|null $expected The expected component
1441 ariadna 441
     * @covers \core\component::get_component_from_classname
1 efrain 442
     */
443
    public function test_get_component_from_classname(
444
        string $classname,
445
        string|null $expected,
446
    ): void {
447
        $this->assertEquals(
448
            $expected,
1441 ariadna 449
            component::get_component_from_classname($classname),
1 efrain 450
        );
451
    }
452
 
453
    /**
454
     * Data provider for get_component_from_classname tests.
455
     *
456
     * @return array
457
     */
458
    public static function get_component_from_classname_provider(): array {
459
        // Start off with testcases which have the leading \.
460
        $testcases = [
461
            // Core.
462
            [\core\example::class, 'core'],
463
 
464
            // A core subsystem.
465
            [\core_message\example::class, 'core_message'],
466
 
467
            // A fake core subsystem.
468
            [\core_fake\example::class, null],
469
 
470
            // A plugin.
471
            [\mod_forum\example::class, 'mod_forum'],
472
 
473
            // A plugin in the old style is not supported.
474
            [\mod_forum_example::class, null],
475
 
476
            // A fake plugin.
477
            [\mod_fake\example::class, null],
478
 
479
            // A subplugin.
480
            [\tiny_link\example::class, 'tiny_link'],
481
        ];
482
 
483
        // Duplicate the testcases, adding a nested namespace.
484
        $testcases = array_merge(
485
            $testcases,
486
            array_map(
487
                fn ($testcase) => [$testcase[0] . '\\in\\sub\\directory', $testcase[1]],
488
                $testcases,
489
            ),
490
        );
491
 
492
        // Duplicate the testcases, removing the leading \.
493
        return array_merge(
494
            $testcases,
495
            array_map(
496
                fn ($testcase) => [ltrim($testcase[0], '\\'), $testcase[1]],
497
                $testcases,
498
            ),
499
        );
500
    }
501
 
502
    public function test_deprecated_get_component_directory(): void {
1441 ariadna 503
        $plugintypes = component::get_plugin_types();
1 efrain 504
        foreach ($plugintypes as $plugintype => $fulldir) {
1441 ariadna 505
            $plugins = component::get_plugin_list($plugintype);
1 efrain 506
            foreach ($plugins as $pluginname => $plugindir) {
507
                $this->assertSame($plugindir, get_component_directory(($plugintype . '_' . $pluginname)));
1441 ariadna 508
                $this->assertDebuggingCalled();
509
                $this->resetDebugging();
1 efrain 510
            }
511
        }
512
 
1441 ariadna 513
        $subsystems = component::get_core_subsystems();
1 efrain 514
        foreach ($subsystems as $subsystem => $fulldir) {
515
            $this->assertSame($fulldir, get_component_directory(('core_' . $subsystem)));
1441 ariadna 516
            $this->assertDebuggingCalled();
517
            $this->resetDebugging();
1 efrain 518
        }
519
    }
520
 
521
    public function test_get_subtype_parent(): void {
522
        global $CFG;
523
 
1441 ariadna 524
        $this->assertNull(component::get_subtype_parent('mod'));
1 efrain 525
 
526
        // Any plugin with more subtypes is ok here.
527
        $this->assertFileExists("$CFG->dirroot/mod/assign/db/subplugins.json");
1441 ariadna 528
        $this->assertSame('mod_assign', component::get_subtype_parent('assignsubmission'));
529
        $this->assertSame('mod_assign', component::get_subtype_parent('assignfeedback'));
530
        $this->assertNull(component::get_subtype_parent('assignxxxxx'));
1 efrain 531
    }
532
 
533
    public function test_get_subplugins(): void {
534
        global $CFG;
535
 
536
        // Any plugin with more subtypes is ok here.
537
        $this->assertFileExists("$CFG->dirroot/mod/assign/db/subplugins.json");
538
 
1441 ariadna 539
        $subplugins = component::get_subplugins('mod_assign');
1 efrain 540
        $this->assertSame(['assignsubmission', 'assignfeedback'], array_keys($subplugins));
541
 
1441 ariadna 542
        $subs = component::get_plugin_list('assignsubmission');
543
        $feeds = component::get_plugin_list('assignfeedback');
1 efrain 544
 
545
        $this->assertSame(array_keys($subs), $subplugins['assignsubmission']);
546
        $this->assertSame(array_keys($feeds), $subplugins['assignfeedback']);
547
 
548
        // Any plugin without subtypes is ok here.
549
        $this->assertFileExists("$CFG->dirroot/mod/choice");
550
        $this->assertFileDoesNotExist("$CFG->dirroot/mod/choice/db/subplugins.json");
551
 
1441 ariadna 552
        $this->assertNull(component::get_subplugins('mod_choice'));
1 efrain 553
 
1441 ariadna 554
        $this->assertNull(component::get_subplugins('xxxx_yyyy'));
1 efrain 555
    }
556
 
557
    public function test_get_plugin_types_with_subplugins(): void {
558
        global $CFG;
559
 
1441 ariadna 560
        $types = component::get_plugin_types_with_subplugins();
1 efrain 561
 
562
        // Hardcode it here to detect if anybody hacks the code to include more subplugin types.
563
        $expected = [
564
            'mod' => "$CFG->dirroot/mod",
565
            'editor' => "$CFG->dirroot/lib/editor",
566
            'tool' => "$CFG->dirroot/$CFG->admin/tool",
567
            'local' => "$CFG->dirroot/local",
568
        ];
569
 
570
        $this->assertSame($expected, $types);
571
    }
572
 
573
    public function test_get_plugin_list_with_file(): void {
574
        $this->resetAfterTest(true);
575
 
1441 ariadna 576
        // No extra reset here because component reset automatically.
1 efrain 577
 
578
        $expected = [];
1441 ariadna 579
        $reports = component::get_plugin_list('report');
1 efrain 580
        foreach ($reports as $name => $fulldir) {
581
            if (file_exists("$fulldir/lib.php")) {
582
                $expected[] = $name;
583
            }
584
        }
585
 
586
        // Test cold.
1441 ariadna 587
        $list = component::get_plugin_list_with_file('report', 'lib.php', false);
1 efrain 588
        $this->assertEquals($expected, array_keys($list));
589
 
590
        // Test hot.
1441 ariadna 591
        $list = component::get_plugin_list_with_file('report', 'lib.php', false);
1 efrain 592
        $this->assertEquals($expected, array_keys($list));
593
 
594
        // Test with include.
1441 ariadna 595
        $list = component::get_plugin_list_with_file('report', 'lib.php', true);
1 efrain 596
        $this->assertEquals($expected, array_keys($list));
597
 
598
        // Test missing.
1441 ariadna 599
        $list = component::get_plugin_list_with_file('report', 'idontexist.php', true);
1 efrain 600
        $this->assertEquals([], array_keys($list));
601
    }
602
 
603
    /**
604
     * Tests for get_component_classes_in_namespace.
605
     */
606
    public function test_get_component_classes_in_namespace(): void {
607
        // Unexisting.
1441 ariadna 608
        $this->assertCount(0, component::get_component_classes_in_namespace('core_unexistingcomponent', 'something'));
609
        $this->assertCount(0, component::get_component_classes_in_namespace('auth_db', 'something'));
1 efrain 610
 
611
        // Matches the last namespace level name not partials.
1441 ariadna 612
        $this->assertCount(0, component::get_component_classes_in_namespace('auth_db', 'tas'));
613
        $this->assertCount(0, component::get_component_classes_in_namespace('core_user', 'course'));
614
        $this->assertCount(0, component::get_component_classes_in_namespace('mod_forum', 'output\\emaildigest'));
615
        $this->assertCount(0, component::get_component_classes_in_namespace('mod_forum', '\\output\\emaildigest'));
1 efrain 616
 
617
        // Without either a component or namespace it returns an empty array.
1441 ariadna 618
        $this->assertEmpty(component::get_component_classes_in_namespace());
619
        $this->assertEmpty(component::get_component_classes_in_namespace(null));
620
        $this->assertEmpty(component::get_component_classes_in_namespace(null, ''));
1 efrain 621
    }
622
 
623
    /**
624
     * Test that the get_component_classes_in_namespace() function returns classes in the correct namespace.
625
     *
626
     * @dataProvider get_component_classes_in_namespace_provider
627
     * @param array $methodargs
628
     * @param string $expectedclassnameformat
629
     */
630
    public function test_get_component_classes_in_namespace_provider(
631
        array $methodargs,
632
        string $expectedclassnameformat,
633
    ): void {
1441 ariadna 634
        $classlist = component::get_component_classes_in_namespace(...$methodargs);
1 efrain 635
        $this->assertGreaterThan(0, count($classlist));
636
 
637
        foreach (array_keys($classlist) as $classname) {
638
            $this->assertStringMatchesFormat($expectedclassnameformat, $classname);
639
        }
640
    }
641
 
642
    /**
643
     * Data provider for get_component_classes_in_namespace tests.
644
     *
645
     * @return array
646
     */
647
    public static function get_component_classes_in_namespace_provider(): array {
648
        return [
649
            // Matches the last namespace level name not partials.
650
            [
651
                ['mod_forum', 'output\\email'],
652
                'mod_forum\output\email\%s',
653
            ],
654
            [
655
                ['mod_forum', '\\output\\email'],
656
                'mod_forum\output\email\%s',
657
            ],
658
            [
659
                ['mod_forum', 'output\\email\\'],
660
                'mod_forum\output\email\%s',
661
            ],
662
            [
663
                ['mod_forum', '\\output\\email\\'],
664
                'mod_forum\output\email\%s',
665
            ],
666
            // Prefix with backslash if it doesn\'t come prefixed.
667
            [
1441 ariadna 668
                ['auth_db', 'task'],
669
                'auth_db\task\%s',
1 efrain 670
            ],
671
            [
1441 ariadna 672
                ['auth_db', '\\task'],
673
                'auth_db\task\%s',
1 efrain 674
            ],
675
 
676
            // Core as a component works, the function can normalise the component name.
677
            [
678
                ['core', 'update'],
679
                'core\update\%s',
680
            ],
681
            [
682
                ['', 'update'],
683
                'core\update\%s',
684
            ],
685
            [
686
                ['moodle', 'update'],
687
                'core\update\%s',
688
            ],
689
 
690
            // Multiple levels.
691
            [
692
                ['core_user', '\\output\\myprofile\\'],
693
                'core_user\output\myprofile\%s',
694
            ],
695
            [
696
                ['core_user', 'output\\myprofile\\'],
697
                'core_user\output\myprofile\%s',
698
            ],
699
            [
700
                ['core_user', '\\output\\myprofile'],
701
                'core_user\output\myprofile\%s',
702
            ],
703
            [
704
                ['core_user', 'output\\myprofile'],
705
                'core_user\output\myprofile\%s',
706
            ],
707
 
708
            // Without namespace it returns classes/ classes.
709
            [
710
                ['tool_mobile', ''],
711
                'tool_mobile\%s',
712
            ],
713
            [
714
                ['tool_filetypes'],
715
                'tool_filetypes\%s',
716
            ],
717
 
718
            // Multiple levels.
719
            [
720
                ['core_user', '\\output\\myprofile\\'],
721
                'core_user\output\myprofile\%s',
722
            ],
723
 
724
            // When no component is specified, classes are returned for the namespace in all components.
725
            // (We don't assert exact amounts here as the count of `output` classes will change depending on plugins installed).
726
            [
727
                ['core', 'output'],
728
                'core\%s',
729
            ],
730
            [
731
                [null, 'output'],
732
                '%s',
733
            ],
734
        ];
735
    }
736
 
737
    /**
738
     * Data provider for classloader test
739
     */
740
    public static function classloader_provider(): array {
741
        global $CFG;
742
 
743
        // As part of these tests, we Check that there are no unexpected problems with overlapping PSR namespaces.
744
        // This is not in the spec, but may come up in some libraries using both namespaces and PEAR-style class names.
745
        // If problems arise we can remove this test, but will need to add a warning.
746
        // Normalise to forward slash for testing purposes.
747
        $directory = str_replace('\\', '/', $CFG->dirroot) . "/lib/tests/fixtures/component/";
748
 
749
        $psr0 = [
750
          'psr0'      => 'lib/tests/fixtures/component/psr0',
751
          'overlap'   => 'lib/tests/fixtures/component/overlap',
752
        ];
753
        $psr4 = [
754
          'psr4'      => 'lib/tests/fixtures/component/psr4',
755
          'overlap'   => 'lib/tests/fixtures/component/overlap',
756
        ];
757
        return [
11 efrain 758
            'PSR-0 Classloading - Root' => [
759
                'psr0' => $psr0,
760
                'psr4' => $psr4,
761
                'classname' => 'psr0_main',
762
                'includedfiles' => "{$directory}psr0/main.php",
763
            ],
764
            'PSR-0 Classloading - Sub namespace - underscores' => [
765
                'psr0' => $psr0,
766
                'psr4' => $psr4,
767
                'classname' => 'psr0_subnamespace_example',
768
                'includedfiles' => "{$directory}psr0/subnamespace/example.php",
769
            ],
770
            'PSR-0 Classloading - Sub namespace - slashes' => [
771
                'psr0' => $psr0,
772
                'psr4' => $psr4,
773
                'classname' => 'psr0\\subnamespace\\slashes',
774
                'includedfiles' => "{$directory}psr0/subnamespace/slashes.php",
775
            ],
776
            'PSR-4 Classloading - Root' => [
777
                'psr0' => $psr0,
778
                'psr4' => $psr4,
779
                'classname' => 'psr4\\main',
780
                'includedfiles' => "{$directory}psr4/main.php",
781
            ],
782
            'PSR-4 Classloading - Sub namespace' => [
783
                'psr0' => $psr0,
784
                'psr4' => $psr4,
785
                'classname' => 'psr4\\subnamespace\\example',
786
                'includedfiles' => "{$directory}psr4/subnamespace/example.php",
787
            ],
788
            'PSR-4 Classloading - Ensure underscores are not converted to paths' => [
789
                'psr0' => $psr0,
790
                'psr4' => $psr4,
791
                'classname' => 'psr4\\subnamespace\\underscore_example',
792
                'includedfiles' => "{$directory}psr4/subnamespace/underscore_example.php",
793
            ],
794
            'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [
795
                'psr0' => $psr0,
796
                'psr4' => $psr4,
797
                'classname' => 'overlap\\subnamespace\\example',
798
                'includedfiles' => "{$directory}overlap/subnamespace/example.php",
799
            ],
800
            'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [
801
                'psr0' => $psr0,
802
                'psr4' => $psr4,
803
                'classname' => 'overlap_subnamespace_example2',
804
                'includedfiles' => "{$directory}overlap/subnamespace/example2.php",
805
            ],
1 efrain 806
        ];
807
    }
808
 
809
    /**
810
     * Test the classloader.
811
     *
812
     * @dataProvider classloader_provider
813
     * @param array $psr0 The PSR-0 namespaces to be used in the test.
814
     * @param array $psr4 The PSR-4 namespaces to be used in the test.
815
     * @param string $classname The name of the class to attempt to load.
816
     * @param string $includedfiles The file expected to be loaded.
817
     * @runInSeparateProcess
818
     */
819
    public function test_classloader($psr0, $psr4, $classname, $includedfiles): void {
1441 ariadna 820
        $psr0namespaces = new ReflectionProperty(component::class, 'psr0namespaces');
1 efrain 821
        $psr0namespaces->setValue(null, $psr0);
822
 
1441 ariadna 823
        $psr4namespaces = new ReflectionProperty(component::class, 'psr4namespaces');
1 efrain 824
        $psr4namespaces->setValue(null, $psr4);
825
 
1441 ariadna 826
        component::classloader($classname);
1 efrain 827
        if (DIRECTORY_SEPARATOR != '/') {
828
            // Denormalise the expected path so that we can quickly compare with get_included_files.
829
            $includedfiles = str_replace('/', DIRECTORY_SEPARATOR, $includedfiles);
830
        }
831
        $this->assertContains($includedfiles, get_included_files());
832
        $this->assertTrue(class_exists($classname, false));
833
    }
834
 
835
    /**
836
     * Data provider for psr_classloader test
837
     */
838
    public static function psr_classloader_provider(): array {
839
        global $CFG;
840
 
841
        // As part of these tests, we Check that there are no unexpected problems with overlapping PSR namespaces.
842
        // This is not in the spec, but may come up in some libraries using both namespaces and PEAR-style class names.
843
        // If problems arise we can remove this test, but will need to add a warning.
844
        // Normalise to forward slash for testing purposes.
845
        $dirroot = str_replace('\\', '/', $CFG->dirroot);
846
        $directory = "{$dirroot}/lib/tests/fixtures/component/";
847
 
848
        $psr0 = [
849
          'psr0'      => 'lib/tests/fixtures/component/psr0',
850
          'overlap'   => 'lib/tests/fixtures/component/overlap',
851
        ];
852
        $psr4 = [
853
          'psr4'      => 'lib/tests/fixtures/component/psr4',
854
          'overlap'   => 'lib/tests/fixtures/component/overlap',
855
        ];
856
        return [
11 efrain 857
            'PSR-0 Classloading - Root' => [
858
                'psr0' => $psr0,
859
                'psr4' => $psr4,
860
                'classname' => 'psr0_main',
861
                'file' => "{$directory}psr0/main.php",
862
            ],
863
            'PSR-0 Classloading - Sub namespace - underscores' => [
864
                'psr0' => $psr0,
865
                'psr4' => $psr4,
866
                'classname' => 'psr0_subnamespace_example',
867
                'file' => "{$directory}psr0/subnamespace/example.php",
868
            ],
869
            'PSR-0 Classloading - Sub namespace - slashes' => [
870
                'psr0' => $psr0,
871
                'psr4' => $psr4,
872
                'classname' => 'psr0\\subnamespace\\slashes',
873
                'file' => "{$directory}psr0/subnamespace/slashes.php",
874
            ],
875
            'PSR-0 Classloading - non-existent file' => [
876
                'psr0' => $psr0,
877
                'psr4' => $psr4,
878
                'classname' => 'psr0_subnamespace_nonexistent_file',
879
                'file' => false,
880
            ],
881
            'PSR-4 Classloading - Root' => [
882
                'psr0' => $psr0,
883
                'psr4' => $psr4,
884
                'classname' => 'psr4\\main',
885
                'file' => "{$directory}psr4/main.php",
886
            ],
887
            'PSR-4 Classloading - Sub namespace' => [
888
                'psr0' => $psr0,
889
                'psr4' => $psr4,
890
                'classname' => 'psr4\\subnamespace\\example',
891
                'file' => "{$directory}psr4/subnamespace/example.php",
892
            ],
893
            'PSR-4 Classloading - Ensure underscores are not converted to paths' => [
894
                'psr0' => $psr0,
895
                'psr4' => $psr4,
896
                'classname' => 'psr4\\subnamespace\\underscore_example',
897
                'file' => "{$directory}psr4/subnamespace/underscore_example.php",
898
            ],
899
            'PSR-4 Classloading - non-existent file' => [
900
                'psr0' => $psr0,
901
                'psr4' => $psr4,
902
                'classname' => 'psr4\\subnamespace\\nonexistent',
903
                'file' => false,
904
            ],
905
            'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [
906
                'psr0' => $psr0,
907
                'psr4' => $psr4,
908
                'classname' => 'overlap\\subnamespace\\example',
909
                'file' => "{$directory}overlap/subnamespace/example.php",
910
            ],
911
            'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [
912
                'psr0' => $psr0,
913
                'psr4' => $psr4,
914
                'classname' => 'overlap_subnamespace_example2',
915
                'file' => "{$directory}overlap/subnamespace/example2.php",
916
            ],
1 efrain 917
            'PSR-4 namespaces can come from multiple sources - first source' => [
918
                'psr0' => $psr0,
919
                'psr4' => [
920
                    'Psr\\Http\\Message' => [
921
                        'lib/psr/http-message/src',
922
                        'lib/psr/http-factory/src',
923
                    ],
924
                ],
925
                'classname' => 'Psr\Http\Message\ServerRequestInterface',
1441 ariadna 926
                'file' => "{$dirroot}/lib/psr/http-message/src/ServerRequestInterface.php",
1 efrain 927
            ],
928
            'PSR-4 namespaces can come from multiple sources - second source' => [
929
                'psr0' => [],
930
                'psr4' => [
931
                    'Psr\\Http\\Message' => [
932
                        'lib/psr/http-message/src',
933
                        'lib/psr/http-factory/src',
934
                    ],
935
                ],
936
                'classname' => 'Psr\Http\Message\ServerRequestFactoryInterface',
1441 ariadna 937
                'file' => "{$dirroot}/lib/psr/http-factory/src/ServerRequestFactoryInterface.php",
1 efrain 938
            ],
939
        ];
940
    }
941
 
942
    /**
11 efrain 943
     * Test that the classloader can load from the test namespaces.
944
     */
945
    public function test_classloader_tests_namespace(): void {
946
        global $CFG;
947
 
948
        $this->resetAfterTest();
949
 
950
        $getclassfilecontent = function (string $classname, ?string $namespace): string {
951
            if ($namespace) {
952
                $content = "<?php\nnamespace $namespace;\nclass $classname {}";
953
            } else {
954
                $content = "<?php\nclass $classname {}";
955
            }
956
            return $content;
957
        };
958
 
959
        $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, [
960
            'lib' => [
961
                'classes' => [
962
                    'example.php' => $getclassfilecontent('example', 'core'),
963
                ],
964
                'tests' => [
965
                    'classes' => [
966
                        'example_classname.php' => $getclassfilecontent('example_classname', \core\tests::class),
967
                    ],
968
                    'behat' => [
969
                        'example_classname.php' => $getclassfilecontent('example_classname', \core\behat::class),
970
                    ],
971
                ],
972
            ],
973
        ]);
974
 
975
        // Note: This is pretty hacky, but it's the only way to test the classloader.
976
        // We have to override the dirroot and libdir, and then reset the plugintypes property.
977
        $CFG->dirroot = $vfileroot->url();
978
        $CFG->libdir = $vfileroot->url() . '/lib';
1441 ariadna 979
        component::reset();
11 efrain 980
 
981
        // Existing classes do not break.
982
        $this->assertTrue(
983
            class_exists(\core\example::class),
984
        );
985
 
986
        // Test and behat classes work.
987
        $this->assertTrue(
988
            class_exists(\core\tests\example_classname::class),
989
        );
990
        $this->assertTrue(
991
            class_exists(\core\behat\example_classname::class),
992
        );
993
 
994
        // Non-existent classes do not do anything.
995
        $this->assertFalse(
996
            class_exists(\core\tests\example_classname_not_found::class),
997
        );
998
    }
999
 
1000
    /**
1 efrain 1001
     * Test the PSR classloader.
1002
     *
1003
     * @dataProvider psr_classloader_provider
1004
     * @param array $psr0 The PSR-0 namespaces to be used in the test.
1005
     * @param array $psr4 The PSR-4 namespaces to be used in the test.
1006
     * @param string $classname The name of the class to attempt to load.
1007
     * @param string|bool $file The expected file corresponding to the class or false for nonexistant.
1008
     * @runInSeparateProcess
1009
     */
1010
    public function test_psr_classloader($psr0, $psr4, $classname, $file): void {
1441 ariadna 1011
        return;
1012
        $psr0namespaces = new ReflectionProperty(component::class, 'psr0namespaces');
1 efrain 1013
        $psr0namespaces->setValue(null, $psr0);
1014
 
1441 ariadna 1015
        $psr4namespaces = new ReflectionProperty(component::class, 'psr4namespaces');
1 efrain 1016
        $psr4namespaces->setValue(null, $psr4);
1017
 
1441 ariadna 1018
        $component = new ReflectionClass(component::class);
1 efrain 1019
        $psrclassloader = $component->getMethod('psr_classloader');
1020
 
1021
        $returnvalue = $psrclassloader->invokeArgs(null, [$classname]);
1022
        // Normalise to forward slashes for testing comparison.
1023
        if ($returnvalue) {
1024
            $returnvalue = str_replace('\\', '/', $returnvalue);
1025
        }
1026
        $this->assertEquals($file, $returnvalue);
1027
    }
1028
 
1029
    /**
1030
     * Data provider for get_class_file test
1031
     */
1032
    public static function get_class_file_provider(): array {
1033
        global $CFG;
1034
 
1035
        return [
1036
          'Getting a file with underscores' => [
1037
              'classname' => 'Test_With_Underscores',
1038
              'prefix' => "Test",
1039
              'path' => 'test/src',
1040
              'separators' => ['_'],
1041
              'result' => $CFG->dirroot . "/test/src/With/Underscores.php",
1042
          ],
1043
          'Getting a file with slashes' => [
1044
              'classname' => 'Test\\With\\Slashes',
1045
              'prefix' => "Test",
1046
              'path' => 'test/src',
1047
              'separators' => ['\\'],
1048
              'result' => $CFG->dirroot . "/test/src/With/Slashes.php",
1049
          ],
1050
          'Getting a file with multiple namespaces' => [
1051
              'classname' => 'Test\\With\\Multiple\\Namespaces',
1052
              'prefix' => "Test\\With",
1053
              'path' => 'test/src',
1054
              'separators' => ['\\'],
1055
              'result' => $CFG->dirroot . "/test/src/Multiple/Namespaces.php",
1056
          ],
1057
          'Getting a file with multiple namespaces (non-existent)' => [
1058
              'classname' => 'Nonexistent\\Namespace\\Test',
1059
              'prefix' => "Test",
1060
              'path' => 'test/src',
1061
              'separators' => ['\\'],
1062
              'result' => false,
1063
          ],
1064
        ];
1065
    }
1066
 
1067
    /**
1068
     * Test the PSR classloader.
1069
     *
1070
     * @dataProvider get_class_file_provider
1071
     * @param string $classname the name of the class.
1072
     * @param string $prefix The namespace prefix used to identify the base directory of the source files.
1073
     * @param string $path The relative path to the base directory of the source files.
1074
     * @param string[] $separators The characters that should be used for separating.
1075
     * @param string|bool $result The expected result to be returned from get_class_file.
1076
     */
1077
    public function test_get_class_file($classname, $prefix, $path, $separators, $result): void {
1441 ariadna 1078
        $component = new ReflectionClass(component::class);
1 efrain 1079
        $psrclassloader = $component->getMethod('get_class_file');
1080
 
1081
        $file = $psrclassloader->invokeArgs(null, [$classname, $prefix, $path, $separators]);
1082
        $this->assertEquals($result, $file);
1083
    }
1084
 
1085
    /**
1086
     * Confirm the get_component_list method contains an entry for every component.
1087
     */
1088
    public function test_get_component_list_contains_all_components(): void {
1089
        global $CFG;
1441 ariadna 1090
        $componentslist = component::get_component_list();
1 efrain 1091
 
1092
        // We should have an entry for each plugin type, and one additional for 'core'.
1441 ariadna 1093
        $plugintypes = component::get_plugin_types();
1 efrain 1094
        $numelementsexpected = count($plugintypes) + 1;
1095
        $this->assertEquals($numelementsexpected, count($componentslist));
1096
 
1097
        // And an entry for each of the plugin types.
1098
        foreach (array_keys($plugintypes) as $plugintype) {
1099
            $this->assertArrayHasKey($plugintype, $componentslist);
1100
        }
1101
 
1102
        // And one for 'core'.
1103
        $this->assertArrayHasKey('core', $componentslist);
1104
 
1105
        // Check a few of the known plugin types to confirm their presence at their respective type index.
1106
        $this->assertEquals($componentslist['core']['core_comment'], $CFG->dirroot . '/comment');
1107
        $this->assertEquals($componentslist['mod']['mod_forum'], $CFG->dirroot . '/mod/forum');
1108
        $this->assertEquals($componentslist['tool']['tool_usertours'], $CFG->dirroot . '/' . $CFG->admin . '/tool/usertours');
1109
    }
1110
 
1111
    /**
1112
     * Test the get_component_names() method.
1113
     *
1114
     * @dataProvider get_component_names_provider
1115
     * @param bool $includecore Whether to include core in the list.
1116
     * @param bool $coreexpected Whether core is expected to be in the list.
1117
     */
1118
    public function test_get_component_names(
1119
        bool $includecore,
1120
        bool $coreexpected,
1121
    ): void {
1122
        global $CFG;
1441 ariadna 1123
        $componentnames = component::get_component_names($includecore);
1 efrain 1124
 
1125
        // We should have an entry for each plugin type.
1441 ariadna 1126
        $plugintypes = component::get_plugin_types();
1 efrain 1127
        $numplugintypes = 0;
1128
        foreach (array_keys($plugintypes) as $type) {
1441 ariadna 1129
            $numplugintypes += count(component::get_plugin_list($type));
1 efrain 1130
        }
1131
        // And an entry for each core subsystem.
1441 ariadna 1132
        $numcomponents = $numplugintypes + count(component::get_core_subsystems());
1 efrain 1133
 
1134
        if ($coreexpected) {
1135
            // Add one for core.
1136
            $numcomponents++;
1137
        }
1138
        $this->assertEquals($numcomponents, count($componentnames));
1139
 
1140
        // Check a few of the known plugin types to confirm their presence at their respective type index.
1141
        $this->assertContains('core_comment', $componentnames);
1142
        $this->assertContains('mod_forum', $componentnames);
1143
        $this->assertContains('tool_usertours', $componentnames);
1144
        $this->assertContains('core_favourites', $componentnames);
1145
        if ($coreexpected) {
1146
            $this->assertContains('core', $componentnames);
1147
        } else {
1148
            $this->assertNotContains('core', $componentnames);
1149
        }
1150
    }
1151
 
1152
    /**
1153
     * Data provider for get_component_names() test.
1154
     *
1155
     * @return array
1156
     */
1157
    public static function get_component_names_provider(): array {
1158
        return [
1159
            [false, false],
1160
            [true, true],
1161
        ];
1162
    }
1163
 
1164
    /**
1441 ariadna 1165
     * Basic tests for APIs related functions in the component class.
1 efrain 1166
     */
1167
    public function test_apis_methods(): void {
1441 ariadna 1168
        $apis = component::get_core_apis();
1 efrain 1169
        $this->assertIsArray($apis);
1170
 
1441 ariadna 1171
        $apinames = component::get_core_api_names();
1 efrain 1172
        $this->assertIsArray($apis);
1173
 
1174
        // Both should return the very same APIs.
1175
        $this->assertEquals($apinames, array_keys($apis));
1176
 
1441 ariadna 1177
        $this->assertFalse(component::is_core_api('lalala'));
1178
        $this->assertTrue(component::is_core_api('privacy'));
1 efrain 1179
    }
1180
 
1181
    /**
1182
     * Test that the apis.json structure matches expectations
1183
     *
1184
     * While we include an apis.schema.json file in core, there isn't any PHP built-in allowing us
1185
     * to validate it (3rd part libraries needed). Plus the schema doesn't allow to validate things
1186
     * like uniqueness or sorting. We are going to do all that here.
1187
     */
1188
    public function test_apis_json_validation(): void {
1441 ariadna 1189
        $apis = $sortedapis = component::get_core_apis();
1 efrain 1190
        ksort($sortedapis); // We'll need this later.
1191
 
1441 ariadna 1192
        $subsystems = component::get_core_subsystems(); // To verify all apis are pointing to valid subsystems.
1 efrain 1193
        $subsystems['core'] = 'anything'; // Let's add 'core' because it's a valid component for apis.
1194
 
1195
        // General structure validations.
1196
        $this->assertIsArray($apis);
1197
        $this->assertGreaterThan(25, count($apis));
1198
        $this->assertArrayHasKey('privacy', $apis); // Verify a few.
1199
        $this->assertArrayHasKey('external', $apis);
1200
        $this->assertArrayHasKey('search', $apis);
1201
        $this->assertEquals(array_keys($sortedapis), array_keys($apis)); // Verify json is sorted alphabetically.
1202
 
1203
        // Iterate over all apis and perform more validations.
1204
        foreach ($apis as $apiname => $attributes) {
1205
            // Message, to be used later and easier finding the problem.
1206
            $message = "Validation problem found with API: {$apiname}";
1207
 
1208
            $this->assertIsObject($attributes, $message);
1209
            $this->assertMatchesRegularExpression('/^[a-z][a-z0-9]+$/', $apiname, $message);
1210
            $this->assertEquals(['component', 'allowedlevel2', 'allowedspread'], array_keys((array)$attributes), $message);
1211
 
1212
            // Verify attributes.
1213
            if ($apiname !== 'core') { // Exception for core api, it doesn't have component.
1214
                // Check that component attribute looks correct.
1215
                $this->assertMatchesRegularExpression('/^(core|[a-z][a-z0-9_]+)$/', $attributes->component, $message);
1216
                // Ensure that the api component (without the core_ prefix) is a correct subsystem.
1217
                $this->assertArrayHasKey(str_replace('core_', '', $attributes->component), $subsystems, $message);
1218
            } else {
1219
                $this->assertNull($attributes->component, $message);
1220
            }
1221
 
1222
 
1223
            // Now check for the rest of attributes.
1224
            $this->assertIsBool($attributes->allowedlevel2, $message);
1225
            $this->assertIsBool($attributes->allowedspread, $message);
1226
 
1227
            // Cannot spread if level2 is not allowed.
1228
            $this->assertLessThanOrEqual($attributes->allowedlevel2, $attributes->allowedspread, $message);
1229
        }
1230
    }
1231
 
1232
    /**
1233
     * Test for monologo icons check in plugins.
1234
     */
1235
    public function test_has_monologo_icon(): void {
1236
        // The Forum activity plugin has monologo icons.
1441 ariadna 1237
        $this->assertTrue(component::has_monologo_icon('mod', 'forum'));
1 efrain 1238
        // The core H5P subsystem doesn't have monologo icons.
1441 ariadna 1239
        $this->assertFalse(component::has_monologo_icon('core', 'h5p'));
1 efrain 1240
        // The function will return false for a non-existent component.
1441 ariadna 1241
        $this->assertFalse(component::has_monologo_icon('randomcomponent', 'h5p'));
1 efrain 1242
    }
1243
 
1244
    /*
1245
     * Tests the getter for the db directory summary hash.
1246
     *
1441 ariadna 1247
     * @covers \core\component::get_all_directory_hashes
1 efrain 1248
     */
1249
    public function test_get_db_directories_hash(): void {
1441 ariadna 1250
        $initial = component::get_all_component_hash();
1 efrain 1251
 
1252
        $dir = make_request_directory();
1441 ariadna 1253
        $hashes = component::get_all_directory_hashes([$dir]);
1254
        $emptydirhash = component::get_all_component_hash([$hashes]);
1 efrain 1255
 
1256
        // Confirm that a single empty directory is a different hash to the core hash.
1257
        $this->assertNotEquals($initial, $emptydirhash);
1258
 
1259
        // Now lets add something to the dir, and check the hash is different.
1260
        $file = fopen($dir . '/test.php', 'w');
1261
        fwrite($file, 'sometestdata');
1262
        fclose($file);
1263
 
1441 ariadna 1264
        $hashes = component::get_all_directory_hashes([$dir]);
1265
        $onefiledirhash = component::get_all_component_hash([$hashes]);
1 efrain 1266
        $this->assertNotEquals($emptydirhash, $onefiledirhash);
1267
 
1268
        // Now add a subdirectory inside the request dir. This should not affect the hash.
1269
        mkdir($dir . '/subdir');
1441 ariadna 1270
        $hashes = component::get_all_directory_hashes([$dir]);
1271
        $finalhash = component::get_all_component_hash([$hashes]);
1 efrain 1272
        $this->assertEquals($onefiledirhash, $finalhash);
1273
    }
1441 ariadna 1274
 
1275
    /**
1276
     * Data provider fetching all third-party lib directories.
1277
     *
1278
     * @return array
1279
     */
1280
    public static function core_thirdparty_libs_provider(): array {
1281
        global $CFG;
1282
 
1283
        $libs = [];
1284
 
1285
        $xmlpath = $CFG->libdir . '/thirdpartylibs.xml';
1286
        $xml = simplexml_load_file($xmlpath);
1287
        foreach ($xml as $lib) {
1288
            $base = realpath(dirname($xmlpath));
1289
            $fullpath = "{$base}/{$lib->location}";
1290
            $relativepath = substr($fullpath, strlen($CFG->dirroot));
1291
 
1292
            $libs[$relativepath] = [
1293
                'name' => (string) $lib->name,
1294
                'fullpath' => $fullpath,
1295
                'relativepath' => $relativepath,
1296
            ];
1297
        }
1298
 
1299
        return $libs;
1300
    }
1301
 
1302
    /**
1303
     * Data provider fetching all third-party lib directories with a composer.json file.
1304
     *
1305
     * @return array
1306
     */
1307
    public static function core_thirdparty_libs_with_composer_provider(): array {
1308
        return array_filter(self::core_thirdparty_libs_provider(), function ($lib) {
1309
            return file_exists("{$lib['fullpath']}/composer.json");
1310
        });
1311
    }
1312
 
1313
    /**
1314
     * Summary of test_composer_files
1315
     *
1316
     * @dataProvider core_thirdparty_libs_with_composer_provider
1317
     * @param string $name
1318
     * @param string $fullpath
1319
     * @param string $relativepath
1320
     */
1321
    public function test_composer_files(
1322
        string $name,
1323
        string $fullpath,
1324
        string $relativepath,
1325
    ): void {
1326
        $this->assertFileExists("{$fullpath}/composer.json");
1327
 
1328
        $composer = json_decode(file_get_contents("{$fullpath}/composer.json"), true);
1329
 
1330
        $rc = new ReflectionClass(\core\component::class);
1331
 
1332
        if (array_key_exists('autoload', $composer)) {
1333
            // Check that the PSR-4 namespaces are present and correct.
1334
            if (array_key_exists('psr-4', $composer['autoload'])) {
1335
                $autoloadnamespaces = $rc->getProperty('psr4namespaces')->getValue(null);
1336
                foreach ($composer['autoload']['psr-4'] as $namespace => $path) {
1337
                    // Composer PSR-4 namespace autoloads may optionally have a trailing slash. Standardise the value.
1338
                    $namespace = rtrim($namespace, '\\');
1339
 
1340
                    // If it exists in the composer.json the namespace must exist in our autoloader.
1341
                    $this->assertArrayHasKey($namespace, $autoloadnamespaces);
1342
 
1343
                    // Ours should be standardised to not have a trailing slash.
1344
                    $this->assertEquals(
1345
                        rtrim($relativepath, '/'),
1346
                        $relativepath,
1347
                        "Moodle PSR-4 namespaces must have no trailing /",
1348
                    );
1349
 
1350
                    // The composer.json can specify an array of possible values.
1351
                    // Standardise the format to the array format.
1352
                    $paths = is_array($path) ? $path : [$path];
1353
 
1354
                    foreach ($paths as $path) {
1355
                        // The composer.json can specify any arbitrary directory within the folder.
1356
                        // It always contains a leading slash (/) or backslash (\) on Windows.
1357
                        // It may also have an optional trailing slash (/).
1358
                        // Concatenate the parts and removes the slashes.
1359
                        $relativenamespacepath = trim("{$relativepath}/{$path}", '/\\');
1360
 
1361
                        // The Moodle PSR-4 autoloader data has two formats:
1362
                        // - a string, for a single source; or
1363
                        // - an array, for multiple sources.
1364
                        // Standardise the format to the latter format.
1365
                        if (!is_array($autoloadnamespaces[$namespace])) {
1366
                            $autoloadnamespaces[$namespace] = [$autoloadnamespaces[$namespace]];
1367
                        }
1368
 
1369
                        // Ensure that the autoloader contains the normalised path.
1370
                        $this->assertContains(
1371
                            $relativenamespacepath,
1372
                            $autoloadnamespaces[$namespace],
1373
                            "Moodle PSR-4 namespace missing entry for library {$name}: {$namespace} => {$relativenamespacepath}",
1374
                        );
1375
                    }
1376
                }
1377
            }
1378
 
1379
            // Check that the composer autoload files are present.
1380
            if (array_key_exists('files', $composer['autoload'])) {
1381
                // The Moodle composer file autoloads are a simple string[].
1382
                $autoloadnamefiles = $rc->getProperty('composerautoloadfiles')->getValue(null);
1383
                foreach ($composer['autoload']['files'] as $file) {
1384
                    $this->assertContains(trim($relativepath, '/\\') . "/{$file}", $autoloadnamefiles);
1385
                }
1386
            }
1387
        }
1388
    }
1389
 
1390
    /**
1391
     * Test that fetching of subtype data throws an exception when a subplugins.php is present without a json equivalent.
1392
     */
1393
    public function test_fetch_subtypes_php_only(): void {
1394
        $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, [
1395
            'plugintype' => [
1396
                'exampleplugin' => [
1397
                    'db' => [
1398
                        'subplugins.php' => '',
1399
                    ],
1400
                ],
1401
            ],
1402
        ]);
1403
 
1404
        $this->expectException(coding_exception::class);
1405
        $this->expectExceptionMessageMatches('/Use of subplugins.php has been deprecated and is no longer supported/');
1406
 
1407
        $pluginroot = $vfileroot->getChild('plugintype/exampleplugin');
1408
 
1409
        $rcm = new \ReflectionMethod(\core\component::class, 'fetch_subtypes');
1410
        $rcm->invoke(null, $pluginroot->url());
1411
    }
1412
 
1413
    /**
1414
     * Test that fetching of subtype data does not throw an exception when a subplugins.php is present
1415
     * with a json file equivalent.
1416
     *
1417
     * Note: The content of the php file is irrelevant and we no longer use it anyway.
1418
     */
1419
    public function test_fetch_subtypes_php_and_json(): void {
1420
        global $CFG;
1421
 
1422
        $this->resetAfterTest();
1423
        $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, [
1424
            'plugintype' => [
1425
                'exampleplugin' => [
1426
                    'db' => [
1427
                        'subplugins.json' => json_encode([
1428
                            'subplugintypes' => [
1429
                                'exampleplugina' => 'apples',
1430
                            ],
1431
                        ]),
1432
                        'subplugins.php' => '',
1433
                    ],
1434
                    'apples' => [],
1435
                ],
1436
            ],
1437
        ]);
1438
 
1439
        $CFG->dirroot = $vfileroot->url();
1440
        $pluginroot = $vfileroot->getChild('plugintype/exampleplugin');
1441
 
1442
        $rcm = new \ReflectionMethod(\core\component::class, 'fetch_subtypes');
1443
        $subplugins = $rcm->invoke(null, $pluginroot->url());
1444
 
1445
        $this->assertEquals([
1446
            'plugintypes' => [
1447
                'exampleplugina' => $pluginroot->getChild('apples')->url(),
1448
            ],
1449
        ], $subplugins);
1450
    }
1451
 
1452
    /**
1453
     * Test that fetching of subtype data in a file which is missing the new subplugintypes key warns.
1454
     */
1455
    public function test_fetch_subtypes_plugintypes_only(): void {
1456
        global $CFG;
1457
 
1458
        $this->resetAfterTest();
1459
        $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, [
1460
            'plugintype' => [
1461
                'exampleplugin' => [
1462
                    'db' => [
1463
                        'subplugins.json' => json_encode([
1464
                            'plugintypes' => [
1465
                                'exampleplugina' => 'plugintype/exampleplugin/apples',
1466
                            ],
1467
                        ]),
1468
                        'subplugins.php' => '',
1469
                    ],
1470
                    'apples' => [],
1471
                ],
1472
            ],
1473
        ]);
1474
 
1475
        $CFG->dirroot = $vfileroot->url();
1476
        $pluginroot = $vfileroot->getChild('plugintype/exampleplugin');
1477
 
1478
        $logdir = make_request_directory();
1479
        $logfile = "{$logdir}/error.log";
1480
        ini_set('error_log', $logfile);
1481
 
1482
        $rcm = new \ReflectionMethod(\core\component::class, 'fetch_subtypes');
1483
        $subplugins = $rcm->invoke(null, $pluginroot->url());
1484
 
1485
        $this->assertEquals([
1486
            'plugintypes' => [
1487
                'exampleplugina' => $pluginroot->getChild('apples')->url(),
1488
            ],
1489
        ], $subplugins);
1490
 
1491
        $warnings = file_get_contents($logfile);
1492
        $this->assertMatchesRegularExpression('/No subplugintypes defined in .*subplugins.json/', $warnings);
1493
    }
1494
 
1495
    /**
1496
     * Ensure that invalid JSON in the subplugins.json file warns appropriately.
1497
     *
1498
     * @dataProvider invalid_subplugins_json_provider
1499
     * @param string[] $expectedwarnings Errors to expect in the exception message
1500
     * @param array[] $json The contents of the subplugins.json file
1501
     */
1502
    public function test_fetch_subtypes_json_invalid_values(
1503
        array $expectedwarnings,
1504
        array $json,
1505
    ): void {
1506
        global $CFG;
1507
 
1508
        $this->resetAfterTest();
1509
        $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, [
1510
            'plugintype' => [
1511
                'exampleplugin' => [
1512
                    'db' => [
1513
                        'subplugins.json' => json_encode($json),
1514
                        'subplugins.php' => '',
1515
                    ],
1516
                    'apples' => [],
1517
                    'pears' => [],
1518
                ],
1519
            ],
1520
        ]);
1521
 
1522
        $CFG->dirroot = $vfileroot->url();
1523
        $pluginroot = $vfileroot->getChild('plugintype/exampleplugin');
1524
 
1525
        $logdir = make_request_directory();
1526
        $logfile = "{$logdir}/error.log";
1527
        ini_set('error_log', $logfile);
1528
 
1529
        $rcm = new \ReflectionMethod(\core\component::class, 'fetch_subtypes');
1530
        $rcm->invoke(null, $pluginroot->url());
1531
 
1532
        $warnings = file_get_contents($logfile);
1533
        foreach ($expectedwarnings as $expectedwarning) {
1534
            $this->assertMatchesRegularExpression($expectedwarning, $warnings);
1535
        }
1536
    }
1537
 
1538
    /**
1539
     * Data provider for invalid subplugins.json files.
1540
     *
1541
     * @return array
1542
     */
1543
    public static function invalid_subplugins_json_provider(): array {
1544
        return [
1545
            'Invalid characters in subtype name' => [
1546
                'expectedwarnings' => [
1547
                    "/Invalid subtype .*APPLES.*detected.*invalid characters present/",
1548
                ],
1549
                'json' => [
1550
                    'subplugintypes' => [
1551
                        'APPLES' => 'plugintype/exampleplugin/apples',
1552
                    ],
1553
                ],
1554
            ],
1555
 
1556
            'Subplugin which duplicates a core subsystem' => [
1557
                'expectedwarnings' => [
1558
                    "/Invalid subtype .*editor.*detected.*duplicates core subsystem/",
1559
                ],
1560
                'json' => [
1561
                    'subplugintypes' => [
1562
                        'editor' => 'apples',
1563
                    ],
1564
                ],
1565
            ],
1566
 
1567
            'Subplugin directory does not exist' => [
1568
                'expectedwarnings' => [
1569
                    "/Invalid subtype directory/",
1570
                ],
1571
                'json' => [
1572
                    'subplugintypes' => [
1573
                        'exampleapples' => 'berries',
1574
                    ],
1575
                ],
1576
            ],
1577
 
1578
            'More subplugintypes than plugintypes' => [
1579
                'expectedwarnings' => [
1580
                    "/Subplugintypes and plugintypes are not in sync/",
1581
                ],
1582
                'json' => [
1583
                    'subplugintypes' => [
1584
                        'apples' => 'pears',
1585
                    ],
1586
                    'plugintypes' => [],
1587
                ],
1588
            ],
1589
 
1590
            'More plugintypes than subplugintypes' => [
1591
                'expectedwarnings' => [
1592
                    "/Subplugintypes and plugintypes are not in sync /",
1593
                ],
1594
                'json' => [
1595
                    'subplugintypes' => [
1596
                        'apples' => 'apples',
1597
                    ],
1598
                    'plugintypes' => [
1599
                        'apples' => 'plugintype/exampleplugin/apples',
1600
                        'pears' => 'plugintype/exampleplugin/pears',
1601
                    ],
1602
                ],
1603
            ],
1604
 
1605
            'subplugintype not defined in plugintype' => [
1606
                'expectedwarnings' => [
1607
                    "/Subplugintypes and plugintypes are not in sync for 'apples'/",
1608
                ],
1609
                'json' => [
1610
                    'subplugintypes' => [
1611
                        'apples' => 'apples',
1612
                    ],
1613
                    'plugintypes' => [
1614
                        'pears' => 'plugintype/exampleplugin/pears',
1615
                    ],
1616
                ],
1617
            ],
1618
            'subplugintype does not match plugintype' => [
1619
                'expectedwarnings' => [
1620
                    "/Subplugintypes and plugintypes are not in sync for 'apples'/",
1621
                ],
1622
                'json' => [
1623
                    'subplugintypes' => [
1624
                        'apples' => 'apples',
1625
                    ],
1626
                    'plugintypes' => [
1627
                        'apples' => 'plugintype/exampleplugin/pears',
1628
                    ],
1629
                ],
1630
            ],
1631
        ];
1632
    }
1633
 
1634
    /**
1635
     * Test various methods when a deprecated plugin type is introduced.
1636
     *
1637
     * @runInSeparateProcess
1638
     * @return void
1639
     */
1640
    public function test_core_component_deprecated_plugintype(): void {
1641
        $this->resetAfterTest();
1642
 
1643
        // Inject the 'fake' plugin type.
1644
        $this->add_full_mocked_plugintype(
1645
            plugintype: 'fake',
1646
            path: 'lib/tests/fixtures/fakeplugins/fake'
1647
        );
1648
 
1649
        $componenthashbefore = component::get_all_component_hash();
1650
        $versionhashbefore = component::get_all_versions_hash();
1651
 
1652
        // Deprecation-specific APIs - pre-deprecation.
1653
        $this->assertArrayHasKey('fake', component::get_plugin_types());
1654
        $this->assertArrayHasKey('fullfeatured', component::get_plugin_list('fake'));
1655
        $this->assertFalse(component::is_deprecated_plugin_type('fake'));
1656
        $this->assertFalse(component::is_deleted_plugin_type('fake'));
1657
        $this->assertFalse(component::is_plugintype_in_deprecation('fake'));
1658
        $this->assertArrayNotHasKey('fake', component::get_deprecated_plugin_types());
1659
        $this->assertArrayNotHasKey('fullfeatured', component::get_deprecated_plugin_list('fake'));
1660
        $this->assertArrayNotHasKey('fake', component::get_deleted_plugin_types());
1661
        $this->assertArrayNotHasKey('fullfeatured', component::get_deleted_plugin_list('fake'));
1662
        $this->assertArrayHasKey('fake', component::get_all_plugin_types());
1663
        $this->assertArrayHasKey('fullfeatured', component::get_all_plugins_list('fake'));
1664
 
1665
        // Deprecate the fake plugintype via mocking component sources.
1666
        $this->deprecate_full_mocked_plugintype('fake');
1667
 
1668
        // Verify before/after hashes have changed, since the plugintype is no longer part of the hash calcs.
1669
        $this->assertNotEquals(component::get_all_component_hash(), $componenthashbefore);
1670
        $this->assertNotEquals(component::get_all_versions_hash(), $versionhashbefore);
1671
 
1672
        // Deprecation-specific APIs - post-deprecation.
1673
        $this->assertTrue(component::is_deprecated_plugin_type('fake'));
1674
        $this->assertFalse(component::is_deleted_plugin_type('fake'));
1675
        $this->assertTrue(component::is_plugintype_in_deprecation('fake'));
1676
        $this->assertArrayHasKey('fake', component::get_deprecated_plugin_types());
1677
        $this->assertArrayHasKey('fullfeatured', component::get_deprecated_plugin_list('fake'));
1678
        $this->assertArrayNotHasKey('fake', component::get_deleted_plugin_types());
1679
        $this->assertArrayNotHasKey('fullfeatured', component::get_deleted_plugin_list('fake'));
1680
        $this->assertArrayHasKey('fake', component::get_all_plugin_types());
1681
        $this->assertArrayHasKey('fullfeatured', component::get_all_plugins_list('fake'));
1682
 
1683
        // Deprecated plugins excluded from the following for B/C.
1684
        $this->assertArrayNotHasKey('fake', component::get_plugin_types());
1685
        $this->assertArrayNotHasKey('fullfeatured', component::get_plugin_list('fake'));
1686
        $this->assertArrayNotHasKey('fake', component::get_component_list());
1687
        $this->assertEmpty(component::get_plugin_list_with_file('fake', 'classes/dummy.php'));
1688
        $this->assertEmpty(component::get_plugin_list_with_class('fake', 'dummy'));
1689
 
1690
        // Deprecated plugins excluded by default for B/C, but can be included by request.
1691
        $this->assertNotContains('fake_fullfeatured', component::get_component_names());
1692
        $this->assertContains('fake_fullfeatured', component::get_component_names(false, true));
1693
 
1694
        // Deprecated plugins included in the following.
1695
        $this->assertIsString(component::get_plugin_directory('fake', 'fullfeatured')); // Used by string manager.
1696
        $this->assertIsString(component::get_component_directory('fake_fullfeatured')); // Uses get_plugin_directory().
1697
        $this->assertTrue(component::has_monologo_icon('fake', 'fullfeatured'));  // Uses get_plugin_directory().
1698
        $this->assertEquals('fake_fullfeatured', component::get_component_from_classname(\fake_fullfeatured\example::class));
1699
 
1700
        // Class autoloading of deprecated plugins is permitted, to facilitate plugin migration code.
1701
        $this->assertArrayHasKey('fake_fullfeatured\dummy',
1702
            component::get_component_classes_in_namespace('fake_fullfeatured'));
1703
        $this->assertTrue(class_exists(\fake_fullfeatured\dummy::class));
1704
    }
1705
 
1706
    /**
1707
     * Test various core_component APIs when dealing with deleted plugin types.
1708
     *
1709
     * @runInSeparateProcess
1710
     * @return void
1711
     */
1712
    public function test_core_component_deleted_plugintype(): void {
1713
        $this->resetAfterTest();
1714
 
1715
        // Inject the 'fake' plugin type.
1716
        $this->add_full_mocked_plugintype(
1717
            plugintype: 'fake',
1718
            path: 'lib/tests/fixtures/fakeplugins/fake',
1719
        );
1720
 
1721
        // Delete the fake plugintype via mocking component sources.
1722
        $this->delete_full_mocked_plugintype('fake');
1723
 
1724
        // Deprecation-specific methods.
1725
        $this->assertFalse(component::is_deprecated_plugin_type('fake'));
1726
        $this->assertTrue(component::is_deleted_plugin_type('fake'));
1727
        $this->assertTrue(component::is_plugintype_in_deprecation('fake'));
1728
        $this->assertArrayNotHasKey('fake', component::get_deprecated_plugin_types());
1729
        $this->assertArrayNotHasKey('fullfeatured', component::get_deprecated_plugin_list('fake'));
1730
        $this->assertArrayHasKey('fake', component::get_deleted_plugin_types());
1731
        $this->assertArrayHasKey('fullfeatured', component::get_deleted_plugin_list('fake'));
1732
        $this->assertArrayHasKey('fake', component::get_all_plugin_types());
1733
        $this->assertArrayHasKey('fullfeatured', component::get_all_plugins_list('fake'));
1734
 
1735
        // Deleted plugintypes/plugins are not included in other methods.
1736
        $this->assertArrayNotHasKey('fake', component::get_plugin_types());
1737
        $this->assertArrayNotHasKey('fullfeatured', component::get_plugin_list('fake'));
1738
        $this->assertNotContains('fake_fullfeatured', component::get_component_names());
1739
        $this->assertNotContains('fake_fullfeatured', component::get_component_names(false, true));
1740
        $this->assertArrayNotHasKey('fake', component::get_component_list());
1741
        $this->assertEmpty(component::get_plugin_list_with_file('fake', 'classes/dummy.php'));
1742
        $this->assertEmpty(component::get_plugin_list_with_class('fake', 'dummy'));
1743
        $this->assertFalse(component::has_monologo_icon('fake', 'fullfeatured'));
1744
        $this->assertNull(component::get_plugin_directory('fake', 'fullfeatured'));
1745
        $this->assertNull(component::get_component_directory('fake_fullfeatured'));
1746
        $this->assertNull(component::get_component_from_classname(\fake_fullfeatured\example::class));
1747
 
1748
        // Class autoloading of deleted plugins is not supported.
1749
        $this->assertArrayNotHasKey('fake_fullfeatured\dummy',
1750
            component::get_component_classes_in_namespace('fake_fullfeatured'));
1751
        $this->assertFalse(class_exists(fake_fullfeatured\dummy::class));
1752
    }
1753
 
1754
    /**
1755
     * Test various core_component APIs when dealing with deprecated subplugins.
1756
     *
1757
     * @runInSeparateProcess
1758
     * @return void
1759
     */
1760
    public function test_core_component_deprecated_subplugintype(): void {
1761
        $this->resetAfterTest();
1762
 
1763
        // Inject the 'fake' plugin type. This includes three mock subplugins:
1764
        // 1. fullsubtype_example: a regular plugin type, not deprecated, nor deleted.
1765
        // 2. fulldeprecatedsubtype_test: a deprecated subplugin type.
1766
        // 3. fulldeletedsubtype_demo: a deleted subplugin type.
1767
        $this->add_full_mocked_plugintype(
1768
            plugintype: 'fake',
1769
            path: 'lib/tests/fixtures/fakeplugins/fake',
1770
            subpluginsupport: true
1771
        );
1772
        $this->assert_deprecation_apis_subplugins();
1773
    }
1774
 
1775
    /**
1776
     * Verify that a plugin which supports subplugins cannot be deprecated.
1777
     *
1778
     * @runInSeparateProcess
1779
     * @return void
1780
     */
1781
    public function test_core_component_deprecated_subplugintype_supporting_subplugins(): void {
1782
        $this->resetAfterTest();
1783
 
1784
        // Inject the 'fake' plugin type. This includes three mock subplugins:
1785
        // 1. fullsubtype_example: a regular plugin type, not deprecated, nor deleted.
1786
        // 2. fulldeprecatedsubtype_test: a deprecated subplugin type.
1787
        // 3. fulldeletedsubtype_demo: a deleted subplugin type.
1788
        $this->add_full_mocked_plugintype(
1789
            plugintype: 'fake',
1790
            path: 'lib/tests/fixtures/fakeplugins/fake',
1791
            subpluginsupport: true
1792
        );
1793
 
1794
        // Try to deprecate the fake plugintype via mocking component sources.
1795
        $this->deprecate_full_mocked_plugintype('fake');
1796
 
1797
        // Deprecation unsupported, so verify core_component treats all plugins the same as before the deprecation attempt.
1798
        // Debugging is expected to be emitted during core_component::init().
1799
        $this->assert_deprecation_apis_subplugins();
1800
        $this->assertDebuggingCalled('Deprecation of a plugin type which supports subplugins is not supported. ' .
1801
            'These plugin types will continue to be treated as active.', DEBUG_DEVELOPER);
1802
    }
1803
 
1804
    /**
1805
     * Verify that a plugin which supports subplugins cannot be deleted.
1806
     *
1807
     * @runInSeparateProcess
1808
     * @return void
1809
     */
1810
    public function test_core_component_deleted_subplugintype_supporting_subplugins(): void {
1811
        $this->resetAfterTest();
1812
 
1813
        // Inject the 'fake' plugin type. This includes three mock subplugins:
1814
        // 1. fullsubtype_example: a regular plugin type, not deprecated, nor deleted.
1815
        // 2. fulldeprecatedsubtype_test: a deprecated subplugin type.
1816
        // 3. fulldeletedsubtype_demo: a deleted subplugin type.
1817
        $this->add_full_mocked_plugintype(
1818
            plugintype: 'fake',
1819
            path: 'lib/tests/fixtures/fakeplugins/fake',
1820
            subpluginsupport: true
1821
        );
1822
 
1823
        // Try to delete the fake plugintype via mocking component sources.
1824
        $this->delete_full_mocked_plugintype('fake');
1825
 
1826
        // Deletion unsupported, so verify core_component treats all plugins the same as before the deletion attempt.
1827
        // Debugging is expected to be emitted during core_component::init().
1828
        $this->assert_deprecation_apis_subplugins();
1829
        $this->assertDebuggingCalled('Deprecation of a plugin type which supports subplugins is not supported. ' .
1830
            'These plugin types will continue to be treated as active.', DEBUG_DEVELOPER);
1831
    }
1832
 
1833
    /**
1834
     * Helper asserting the returns for various core_component APIs when dealing with deprecated and deleted subplugins.
1835
     *
1836
     * @return void
1837
     */
1838
    protected function assert_deprecation_apis_subplugins(): void {
1839
        // Deprecation-specific methods.
1840
        $this->assertFalse(component::is_deprecated_plugin_type('fake'));
1841
        $this->assertFalse(component::is_deprecated_plugin_type('fullsubtype'));
1842
        $this->assertTrue(component::is_deprecated_plugin_type('fulldeprecatedsubtype'));
1843
        $this->assertFalse(component::is_deprecated_plugin_type('fulldeletedsubtype'));
1844
 
1845
        $this->assertFalse(component::is_deleted_plugin_type('fake'));
1846
        $this->assertFalse(component::is_deleted_plugin_type('fullsubtype'));
1847
        $this->assertFalse(component::is_deleted_plugin_type('fulldeprecatedsubtype'));
1848
        $this->assertTrue(component::is_deleted_plugin_type('fulldeletedsubtype'));
1849
 
1850
        $this->assertFalse(component::is_plugintype_in_deprecation('fake'));
1851
        $this->assertFalse(component::is_plugintype_in_deprecation('fullsubtype'));
1852
        $this->assertTrue(component::is_plugintype_in_deprecation('fulldeprecatedsubtype'));
1853
        $this->assertTrue(component::is_plugintype_in_deprecation('fulldeletedsubtype'));
1854
 
1855
        $this->assertArrayNotHasKey('fake', component::get_deprecated_plugin_types());
1856
        $this->assertArrayNotHasKey('fullsubtype', component::get_deprecated_plugin_types());
1857
        $this->assertArrayHasKey('fulldeprecatedsubtype', component::get_deprecated_plugin_types());
1858
        $this->assertArrayNotHasKey('fulldeletedsubtype', component::get_deprecated_plugin_types());
1859
 
1860
        $this->assertArrayNotHasKey('fullfeatured', component::get_deprecated_plugin_list('fake'));
1861
        $this->assertArrayNotHasKey('example', component::get_deprecated_plugin_list('fullsubtype'));
1862
        $this->assertArrayHasKey('test', component::get_deprecated_plugin_list('fulldeprecatedsubtype'));
1863
        $this->assertArrayNotHasKey('demo', component::get_deprecated_plugin_list('fulldeletedsubtype'));
1864
 
1865
        $this->assertArrayNotHasKey('fake', component::get_deleted_plugin_types());
1866
        $this->assertArrayNotHasKey('fullsubtype', component::get_deleted_plugin_types());
1867
        $this->assertArrayNotHasKey('fulldeprecatedsubtype', component::get_deleted_plugin_types());
1868
        $this->assertArrayHasKey('fulldeletedsubtype', component::get_deleted_plugin_types());
1869
 
1870
        $this->assertArrayNotHasKey('fullfeatured', component::get_deleted_plugin_list('fake'));
1871
        $this->assertArrayNotHasKey('example', component::get_deleted_plugin_list('fullsubtype'));
1872
        $this->assertArrayNotHasKey('test', component::get_deleted_plugin_list('fulldeprecatedsubtype'));
1873
        $this->assertArrayHasKey('demo', component::get_deleted_plugin_list('fulldeletedsubtype'));
1874
 
1875
        $this->assertArrayHasKey('fake', component::get_all_plugin_types());
1876
        $this->assertArrayHasKey('fullsubtype', component::get_all_plugin_types());
1877
        $this->assertArrayHasKey('fulldeprecatedsubtype', component::get_all_plugin_types());
1878
        $this->assertArrayHasKey('fulldeletedsubtype', component::get_all_plugin_types());
1879
 
1880
        $this->assertArrayHasKey('fullfeatured', component::get_all_plugins_list('fake'));
1881
        $this->assertArrayHasKey('example', component::get_all_plugins_list('fullsubtype'));
1882
        $this->assertArrayHasKey('test', component::get_all_plugins_list('fulldeprecatedsubtype'));
1883
        $this->assertArrayHasKey('demo', component::get_all_plugins_list('fulldeletedsubtype'));
1884
 
1885
        // Deprecated and deleted plugins excluded from the following for B/C.
1886
        $this->assertArrayHasKey('fake', component::get_plugin_types());
1887
        $this->assertArrayHasKey('fullsubtype', component::get_plugin_types());
1888
        $this->assertArrayNotHasKey('fulldeprecatedsubtype', component::get_plugin_types());
1889
        $this->assertArrayNotHasKey('fulldeletedsubtype', component::get_plugin_types());
1890
 
1891
        $this->assertNotEmpty(component::get_plugin_list('fake'));
1892
        $this->assertNotEmpty(component::get_plugin_list('fullsubtype'));
1893
        $this->assertEmpty(component::get_plugin_list('fulldeprecatedsubtype'));
1894
        $this->assertEmpty(component::get_plugin_list('fulldeletedsubtype'));
1895
 
1896
        $this->assertArrayHasKey('fake', component::get_component_list());
1897
        $this->assertArrayHasKey('fullsubtype', component::get_component_list());
1898
        $this->assertArrayNotHasKey('fulldeprecatedsubtype', component::get_component_list());
1899
        $this->assertArrayNotHasKey('fulldeletedsubtype', component::get_component_list());
1900
 
1901
        $this->assertArrayHasKey('fullfeatured', component::get_plugin_list_with_file('fake', 'classes/dummy.php'));
1902
        $this->assertArrayHasKey('example', component::get_plugin_list_with_file('fullsubtype', 'classes/dummy.php'));
1903
        $this->assertEmpty(component::get_plugin_list_with_file('fulldeprecatedsubtype', 'classes/dummy.php'));
1904
        $this->assertEmpty(component::get_plugin_list_with_file('fulldeletedsubtype', 'classes/dummy.php'));
1905
 
1906
        $this->assertArrayHasKey('fake_fullfeatured', component::get_plugin_list_with_class('fake', 'dummy'));
1907
        $this->assertArrayHasKey('fullsubtype_example', component::get_plugin_list_with_class('fullsubtype', 'dummy'));
1908
        $this->assertEmpty(component::get_plugin_list_with_class('fulldeprecatedsubtype', 'dummy'));
1909
        $this->assertEmpty(component::get_plugin_list_with_class('fulldeletedsubtype', 'dummy'));
1910
 
1911
        $this->assertArrayHasKey('fullsubtype', component::get_subplugins('fake_fullfeatured'));
1912
        $this->assertContains('example', component::get_subplugins('fake_fullfeatured')['fullsubtype']);
1913
        $this->assertArrayNotHasKey('fulldeprecatedsubtype', component::get_subplugins('fake_fullfeatured'));
1914
        $this->assertArrayNotHasKey('fulldeletedsubtype', component::get_subplugins('fake_fullfeatured'));
1915
 
1916
        $this->assertArrayHasKey('fullsubtype', component::get_all_subplugins('fake_fullfeatured'));
1917
        $this->assertContains('example', component::get_all_subplugins('fake_fullfeatured')['fullsubtype']);
1918
        $this->assertContains('test', component::get_all_subplugins('fake_fullfeatured')['fulldeprecatedsubtype']);
1919
        $this->assertContains('demo', component::get_all_subplugins('fake_fullfeatured')['fulldeletedsubtype']);
1920
 
1921
        // Deprecated plugins excluded by default for B/C, but can be included by request.
1922
        // Deleted plugins are always excluded.
1923
        $this->assertContains('fake_fullfeatured', component::get_component_names());
1924
        $this->assertContains('fullsubtype_example', component::get_component_names());
1925
        $this->assertNotContains('fulldeprecatedsubtype_test', component::get_component_names());
1926
        $this->assertNotContains('fulldeletedsubtype_demo', component::get_component_names());
1927
        $this->assertContains('fulldeprecatedsubtype_test', component::get_component_names(false, true));
1928
        $this->assertNotContains('fulldeletedsubtype_demo', component::get_component_names(false, true));
1929
 
1930
        // Deprecated plugins included in the following, but deleted plugins are excluded.
1931
        $this->assertIsString(component::get_plugin_directory('fake', 'fullfeatured')); // Used by string manager.
1932
        $this->assertIsString(component::get_plugin_directory('fullsubtype', 'example'));
1933
        $this->assertIsString(component::get_plugin_directory('fulldeprecatedsubtype', 'test'));
1934
        $this->assertNull(component::get_plugin_directory('fulldeletedsubtype', 'demo'));
1935
 
1936
        $this->assertIsString(component::get_component_directory('fake_fullfeatured')); // Uses get_plugin_directory().
1937
        $this->assertIsString(component::get_component_directory('fullsubtype_example'));
1938
        $this->assertIsString(component::get_component_directory('fulldeprecatedsubtype_test'));
1939
        $this->assertNull(component::get_component_directory('fulldeletedsubtype_demo'));
1940
 
1941
        $this->assertTrue(component::has_monologo_icon('fullsubtype', 'example')); // Uses get_plugin_directory().
1942
        $this->assertTrue(component::has_monologo_icon('fulldeprecatedsubtype', 'test'));
1943
        $this->assertFalse(component::has_monologo_icon('fulldeletedsubtype', 'demo'));
1944
 
1945
        $this->assertEquals('fake_fullfeatured', component::get_component_from_classname(\fake_fullfeatured\example::class));
1946
        $this->assertEquals('fullsubtype_example',
1947
            component::get_component_from_classname(\fullsubtype_example\example::class));
1948
        $this->assertEquals('fulldeprecatedsubtype_test',
1949
            component::get_component_from_classname(\fulldeprecatedsubtype_test\example::class));
1950
        $this->assertNull(component::get_component_from_classname(\fulldeletedsubtype_demo\example::class));
1951
 
1952
        // Deprecated and deleted plugins included in the following.
1953
        $this->assertEquals('fake_fullfeatured', component::get_subtype_parent('fullsubtype'));
1954
        $this->assertEquals('fake_fullfeatured', component::get_subtype_parent('fulldeprecatedsubtype'));
1955
        $this->assertEquals('fake_fullfeatured', component::get_subtype_parent('fulldeletedsubtype'));
1956
 
1957
        // Class autoloading of deprecated plugins is permitted, to facilitate plugin migration code, but not for deleted plugins.
1958
        $this->assertArrayHasKey('fake_fullfeatured\dummy',
1959
            component::get_component_classes_in_namespace('fake_fullfeatured'));
1960
        $this->assertArrayHasKey('fullsubtype_example\dummy',
1961
            component::get_component_classes_in_namespace('fullsubtype_example'));
1962
        $this->assertArrayHasKey('fulldeprecatedsubtype_test\dummy',
1963
            component::get_component_classes_in_namespace('fulldeprecatedsubtype_test'));
1964
        $this->assertEquals([], component::get_component_classes_in_namespace('fulldeletedsubtype_demo'));
1965
 
1966
        $this->assertTrue(class_exists(\fake_fullfeatured\dummy::class));
1967
        $this->assertTrue(class_exists(\fullsubtype_example\dummy::class));
1968
        $this->assertTrue(class_exists(\fulldeprecatedsubtype_test\dummy::class));
1969
        $this->assertFalse(class_exists(\fulldeletedsubtype_demo\dummy::class));
1970
    }
1 efrain 1971
}