Proyectos de Subversion Moodle

Rev

Rev 1 | | 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
 
17
// phpcs:disable moodle.PHPUnit.TestCaseNames.MissingNS
18
 
19
/**
20
 * core_component related tests.
21
 *
22
 * @package    core
23
 * @category   test
24
 * @copyright  2013 Petr Skoda {@link http://skodak.org}
25
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26
 *
27
 * @covers \core_component
28
 */
29
final class component_test extends advanced_testcase {
30
    /**
31
     * To be changed if number of subsystems increases/decreases,
32
     * this is defined here to annoy devs that try to add more without any thinking,
33
     * always verify that it does not collide with any existing add-on modules and subplugins!!!
34
     */
35
    const SUBSYSTEMCOUNT = 77;
36
 
11 efrain 37
    #[\Override]
38
    public function tearDown(): void {
39
        parent::tearDown();
40
 
41
        \core_component::reset();
42
    }
43
 
1 efrain 44
    public function test_get_core_subsystems(): void {
45
        global $CFG;
46
 
47
        $subsystems = core_component::get_core_subsystems();
48
 
49
        $this->assertCount(
50
            self::SUBSYSTEMCOUNT,
51
            $subsystems,
52
            'Oh, somebody added or removed a core subsystem, think twice before doing that!',
53
        );
54
 
55
        // Make sure all paths are full/null, exist and are inside dirroot.
56
        foreach ($subsystems as $subsystem => $fulldir) {
57
            $this->assertFalse(strpos($subsystem, '_'), 'Core subsystems must be one work without underscores');
58
            if ($fulldir === null) {
59
                if ($subsystem === 'filepicker' || $subsystem === 'help') { // phpcs:ignore
60
                    // Arrgghh, let's not introduce more subsystems for no real reason...
61
                } else {
62
                    // Lang strings.
63
                    $this->assertFileExists(
64
                        "$CFG->dirroot/lang/en/$subsystem.php",
65
                        'Core subsystems without fulldir are usually used for lang strings.',
66
                    );
67
                }
68
                continue;
69
            }
70
            $this->assertFileExists($fulldir);
71
            // Check that base uses realpath() separators and "/" in the subdirs.
72
            $this->assertStringStartsWith($CFG->dirroot . '/', $fulldir);
73
            $reldir = substr($fulldir, strlen($CFG->dirroot) + 1);
74
            $this->assertFalse(strpos($reldir, '\\'));
75
        }
76
 
77
        // Make sure all core language files are also subsystems!
78
        $items = new DirectoryIterator("$CFG->dirroot/lang/en");
79
        foreach ($items as $item) {
80
            if ($item->isDot() || $item->isDir()) {
81
                continue;
82
            }
83
            $file = $item->getFilename();
84
            if ($file === 'moodle.php') {
85
                // Do not add new lang strings unless really necessary!!!
86
                continue;
87
            }
88
 
89
            if (substr($file, -4) !== '.php') {
90
                continue;
91
            }
92
            $file = substr($file, 0, strlen($file) - 4);
93
            $this->assertArrayHasKey(
94
                $file,
95
                $subsystems,
96
                'All core lang files should be subsystems, think twice before adding anything!',
97
            );
98
        }
99
        unset($item);
100
        unset($items);
101
    }
102
 
103
    public function test_deprecated_get_core_subsystems(): void {
104
        global $CFG;
105
 
106
        $subsystems = core_component::get_core_subsystems();
107
 
108
        $this->assertSame($subsystems, get_core_subsystems(true));
109
 
110
        $realsubsystems = get_core_subsystems();
111
        $this->assertDebuggingCalled();
112
        $this->assertSame($realsubsystems, get_core_subsystems(false));
113
        $this->assertDebuggingCalled();
114
 
115
        $this->assertEquals(count($subsystems), count($realsubsystems));
116
 
117
        foreach ($subsystems as $subsystem => $fulldir) {
118
            $this->assertArrayHasKey($subsystem, $realsubsystems);
119
            if ($fulldir === null) {
120
                $this->assertNull($realsubsystems[$subsystem]);
121
                continue;
122
            }
123
            $this->assertSame($fulldir, $CFG->dirroot . '/' . $realsubsystems[$subsystem]);
124
        }
125
    }
126
 
127
    public function test_get_plugin_types(): void {
128
        global $CFG;
129
 
130
        $this->assertTrue(
131
            empty($CFG->themedir),
132
            'Non-empty $CFG->themedir is not covered by any tests yet, you need to disable it.',
133
        );
134
 
135
        $plugintypes = core_component::get_plugin_types();
136
 
137
        foreach ($plugintypes as $plugintype => $fulldir) {
138
            $this->assertStringStartsWith("$CFG->dirroot/", $fulldir);
139
        }
140
    }
141
 
142
    public function test_deprecated_get_plugin_types(): void {
143
        global $CFG;
144
 
145
        $plugintypes = core_component::get_plugin_types();
146
 
147
        $this->assertSame($plugintypes, get_plugin_types());
148
        $this->assertSame($plugintypes, get_plugin_types(true));
149
 
150
        $realplugintypes = get_plugin_types(false);
151
        $this->assertDebuggingCalled();
152
 
153
        foreach ($plugintypes as $plugintype => $fulldir) {
154
            $this->assertSame($fulldir, $CFG->dirroot . '/' . $realplugintypes[$plugintype]);
155
        }
156
    }
157
 
158
    public function test_get_plugin_list(): void {
159
        global $CFG;
160
 
161
        $plugintypes = core_component::get_plugin_types();
162
 
163
        foreach ($plugintypes as $plugintype => $fulldir) {
164
            $plugins = core_component::get_plugin_list($plugintype);
165
            foreach ($plugins as $pluginname => $plugindir) {
166
                $this->assertStringStartsWith("$CFG->dirroot/", $plugindir);
167
            }
168
            if ($plugintype !== 'auth') {
169
                // Let's crosscheck it with independent implementation (auth/db is an exception).
170
                $reldir = substr($fulldir, strlen($CFG->dirroot) + 1);
171
                $dirs = get_list_of_plugins($reldir);
172
                $dirs = array_values($dirs);
173
                $this->assertDebuggingCalled();
174
                $this->assertSame($dirs, array_keys($plugins));
175
            }
176
        }
177
    }
178
 
179
    public function test_deprecated_get_plugin_list(): void {
180
        $plugintypes = core_component::get_plugin_types();
181
 
182
        foreach ($plugintypes as $plugintype => $fulldir) {
183
            $plugins = core_component::get_plugin_list($plugintype);
184
            $this->assertSame($plugins, get_plugin_list($plugintype));
185
        }
186
    }
187
 
188
    public function test_get_plugin_directory(): void {
189
        $plugintypes = core_component::get_plugin_types();
190
 
191
        foreach ($plugintypes as $plugintype => $fulldir) {
192
            $plugins = core_component::get_plugin_list($plugintype);
193
            foreach ($plugins as $pluginname => $plugindir) {
194
                $this->assertSame($plugindir, core_component::get_plugin_directory($plugintype, $pluginname));
195
            }
196
        }
197
    }
198
 
199
    public function test_deprecated_get_plugin_directory(): void {
200
        $plugintypes = core_component::get_plugin_types();
201
 
202
        foreach ($plugintypes as $plugintype => $fulldir) {
203
            $plugins = core_component::get_plugin_list($plugintype);
204
            foreach ($plugins as $pluginname => $plugindir) {
205
                $this->assertSame(
206
                    core_component::get_plugin_directory($plugintype, $pluginname),
207
                    get_plugin_directory($plugintype, $pluginname),
208
                );
209
            }
210
        }
211
    }
212
 
213
    public function test_get_subsystem_directory(): void {
214
        $subsystems = core_component::get_core_subsystems();
215
        foreach ($subsystems as $subsystem => $fulldir) {
216
            $this->assertSame($fulldir, core_component::get_subsystem_directory($subsystem));
217
        }
218
    }
219
 
220
    /**
221
     * Test that the get_plugin_list_with_file() function returns the correct list of plugins.
222
     *
223
     * @covers \core_component::is_valid_plugin_name
224
     * @dataProvider is_valid_plugin_name_provider
225
     * @param array $arguments
226
     * @param bool $expected
227
     */
228
    public function test_is_valid_plugin_name(array $arguments, bool $expected): void {
229
        $this->assertEquals($expected, core_component::is_valid_plugin_name(...$arguments));
230
    }
231
 
232
    /**
233
     * Data provider for the is_valid_plugin_name function.
234
     *
235
     * @return array
236
     */
237
    public static function is_valid_plugin_name_provider(): array {
238
        return [
239
            [['mod', 'example1'], true],
240
            [['mod', 'feedback360'], true],
241
            [['mod', 'feedback_360'], false],
242
            [['mod', '2feedback'], false],
243
            [['mod', '1example'], false],
244
            [['mod', 'example.xx'], false],
245
            [['mod', '.example'], false],
246
            [['mod', '_example'], false],
247
            [['mod', 'example_'], false],
248
            [['mod', 'example_x1'], false],
249
            [['mod', 'example-x1'], false],
250
            [['mod', 'role'], false],
251
 
252
            [['tool', 'example1'], true],
253
            [['tool', 'example_x1'], true],
254
            [['tool', 'example_x1_xxx'], true],
255
            [['tool', 'feedback360'], true],
256
            [['tool', 'feed_back360'], true],
257
            [['tool', 'role'], true],
258
            [['tool', '1example'], false],
259
            [['tool', 'example.xx'], false],
260
            [['tool', 'example-xx'], false],
261
            [['tool', '.example'], false],
262
            [['tool', '_example'], false],
263
            [['tool', 'example_'], false],
264
            [['tool', 'example__x1'], false],
265
 
266
            // Some invalid cases.
267
            [['mod', null], false],
268
            [['mod', ''], false],
269
            [['tool', null], false],
270
            [['tool', ''], false],
271
        ];
272
    }
273
 
274
    public function test_normalize_componentname(): void {
275
        // Moodle core.
276
        $this->assertSame('core', core_component::normalize_componentname('core'));
277
        $this->assertSame('core', core_component::normalize_componentname('moodle'));
278
        $this->assertSame('core', core_component::normalize_componentname(''));
279
 
280
        // Moodle core subsystems.
281
        $this->assertSame('core_admin', core_component::normalize_componentname('admin'));
282
        $this->assertSame('core_admin', core_component::normalize_componentname('core_admin'));
283
        $this->assertSame('core_admin', core_component::normalize_componentname('moodle_admin'));
284
 
285
        // Activity modules and their subplugins.
286
        $this->assertSame('mod_workshop', core_component::normalize_componentname('workshop'));
287
        $this->assertSame('mod_workshop', core_component::normalize_componentname('mod_workshop'));
288
        $this->assertSame('workshopform_accumulative', core_component::normalize_componentname('workshopform_accumulative'));
289
        $this->assertSame('mod_quiz', core_component::normalize_componentname('quiz'));
290
        $this->assertSame('quiz_grading', core_component::normalize_componentname('quiz_grading'));
291
        $this->assertSame('mod_data', core_component::normalize_componentname('data'));
292
        $this->assertSame('datafield_checkbox', core_component::normalize_componentname('datafield_checkbox'));
293
 
294
        // Other plugin types.
295
        $this->assertSame('auth_mnet', core_component::normalize_componentname('auth_mnet'));
296
        $this->assertSame('enrol_self', core_component::normalize_componentname('enrol_self'));
297
        $this->assertSame('block_html', core_component::normalize_componentname('block_html'));
298
        $this->assertSame('block_mnet_hosts', core_component::normalize_componentname('block_mnet_hosts'));
299
        $this->assertSame('local_amos', core_component::normalize_componentname('local_amos'));
300
        $this->assertSame('local_admin', core_component::normalize_componentname('local_admin'));
301
 
302
        // Unknown words without underscore are supposed to be activity modules.
303
        $this->assertSame(
304
            'mod_whoonearthwouldcomewithsuchastupidnameofcomponent',
305
            core_component::normalize_componentname('whoonearthwouldcomewithsuchastupidnameofcomponent')
306
        );
307
        // Module names can not contain underscores, this must be a subplugin.
308
        $this->assertSame(
309
            'whoonearth_wouldcomewithsuchastupidnameofcomponent',
310
            core_component::normalize_componentname('whoonearth_wouldcomewithsuchastupidnameofcomponent')
311
        );
312
        $this->assertSame(
313
            'whoonearth_would_come_withsuchastupidnameofcomponent',
314
            core_component::normalize_componentname('whoonearth_would_come_withsuchastupidnameofcomponent')
315
        );
316
    }
317
 
318
    public function test_normalize_component(): void {
319
        // Moodle core.
320
        $this->assertSame(['core', null], core_component::normalize_component('core'));
321
        $this->assertSame(['core', null], core_component::normalize_component('moodle'));
322
        $this->assertSame(['core', null], core_component::normalize_component(''));
323
 
324
        // Moodle core subsystems.
325
        $this->assertSame(['core', 'admin'], core_component::normalize_component('admin'));
326
        $this->assertSame(['core', 'admin'], core_component::normalize_component('core_admin'));
327
        $this->assertSame(['core', 'admin'], core_component::normalize_component('moodle_admin'));
328
 
329
        // Activity modules and their subplugins.
330
        $this->assertSame(['mod', 'workshop'], core_component::normalize_component('workshop'));
331
        $this->assertSame(['mod', 'workshop'], core_component::normalize_component('mod_workshop'));
332
        $this->assertSame(['workshopform', 'accumulative'], core_component::normalize_component('workshopform_accumulative'));
333
        $this->assertSame(['mod', 'quiz'], core_component::normalize_component('quiz'));
334
        $this->assertSame(['quiz', 'grading'], core_component::normalize_component('quiz_grading'));
335
        $this->assertSame(['mod', 'data'], core_component::normalize_component('data'));
336
        $this->assertSame(['datafield', 'checkbox'], core_component::normalize_component('datafield_checkbox'));
337
 
338
        // Other plugin types.
339
        $this->assertSame(['auth', 'mnet'], core_component::normalize_component('auth_mnet'));
340
        $this->assertSame(['enrol', 'self'], core_component::normalize_component('enrol_self'));
341
        $this->assertSame(['block', 'html'], core_component::normalize_component('block_html'));
342
        $this->assertSame(['block', 'mnet_hosts'], core_component::normalize_component('block_mnet_hosts'));
343
        $this->assertSame(['local', 'amos'], core_component::normalize_component('local_amos'));
344
        $this->assertSame(['local', 'admin'], core_component::normalize_component('local_admin'));
345
 
346
        // Unknown words without underscore are supposed to be activity modules.
347
        $this->assertSame(
348
            ['mod', 'whoonearthwouldcomewithsuchastupidnameofcomponent'],
349
            core_component::normalize_component('whoonearthwouldcomewithsuchastupidnameofcomponent')
350
        );
351
        // Module names can not contain underscores, this must be a subplugin.
352
        $this->assertSame(
353
            ['whoonearth', 'wouldcomewithsuchastupidnameofcomponent'],
354
            core_component::normalize_component('whoonearth_wouldcomewithsuchastupidnameofcomponent')
355
        );
356
        $this->assertSame(
357
            ['whoonearth', 'would_come_withsuchastupidnameofcomponent'],
358
            core_component::normalize_component('whoonearth_would_come_withsuchastupidnameofcomponent')
359
        );
360
    }
361
 
362
    public function test_deprecated_normalize_component(): void {
363
        // Moodle core.
364
        $this->assertSame(['core', null], normalize_component('core'));
365
        $this->assertSame(['core', null], normalize_component(''));
366
        $this->assertSame(['core', null], normalize_component('moodle'));
367
 
368
        // Moodle core subsystems.
369
        $this->assertSame(['core', 'admin'], normalize_component('admin'));
370
        $this->assertSame(['core', 'admin'], normalize_component('core_admin'));
371
        $this->assertSame(['core', 'admin'], normalize_component('moodle_admin'));
372
 
373
        // Activity modules and their subplugins.
374
        $this->assertSame(['mod', 'workshop'], normalize_component('workshop'));
375
        $this->assertSame(['mod', 'workshop'], normalize_component('mod_workshop'));
376
        $this->assertSame(['workshopform', 'accumulative'], normalize_component('workshopform_accumulative'));
377
        $this->assertSame(['mod', 'quiz'], normalize_component('quiz'));
378
        $this->assertSame(['quiz', 'grading'], normalize_component('quiz_grading'));
379
        $this->assertSame(['mod', 'data'], normalize_component('data'));
380
        $this->assertSame(['datafield', 'checkbox'], normalize_component('datafield_checkbox'));
381
 
382
        // Other plugin types.
383
        $this->assertSame(['auth', 'mnet'], normalize_component('auth_mnet'));
384
        $this->assertSame(['enrol', 'self'], normalize_component('enrol_self'));
385
        $this->assertSame(['block', 'html'], normalize_component('block_html'));
386
        $this->assertSame(['block', 'mnet_hosts'], normalize_component('block_mnet_hosts'));
387
        $this->assertSame(['local', 'amos'], normalize_component('local_amos'));
388
        $this->assertSame(['local', 'admin'], normalize_component('local_admin'));
389
 
390
        // Unknown words without underscore are supposed to be activity modules.
391
        $this->assertSame(
392
            ['mod', 'whoonearthwouldcomewithsuchastupidnameofcomponent'],
393
            normalize_component('whoonearthwouldcomewithsuchastupidnameofcomponent')
394
        );
395
        // Module names can not contain underscores, this must be a subplugin.
396
        $this->assertSame(
397
            ['whoonearth', 'wouldcomewithsuchastupidnameofcomponent'],
398
            normalize_component('whoonearth_wouldcomewithsuchastupidnameofcomponent')
399
        );
400
        $this->assertSame(
401
            ['whoonearth', 'would_come_withsuchastupidnameofcomponent'],
402
            normalize_component('whoonearth_would_come_withsuchastupidnameofcomponent')
403
        );
404
    }
405
 
406
    public function test_get_component_directory(): void {
407
        $plugintypes = core_component::get_plugin_types();
408
        foreach ($plugintypes as $plugintype => $fulldir) {
409
            $plugins = core_component::get_plugin_list($plugintype);
410
            foreach ($plugins as $pluginname => $plugindir) {
411
                $this->assertSame($plugindir, core_component::get_component_directory(($plugintype . '_' . $pluginname)));
412
            }
413
        }
414
 
415
        $subsystems = core_component::get_core_subsystems();
416
        foreach ($subsystems as $subsystem => $fulldir) {
417
            $this->assertSame($fulldir, core_component::get_component_directory(('core_' . $subsystem)));
418
        }
419
    }
420
 
421
    /**
422
     * Unit tests for get_component_from_classname.
423
     *
424
     * @dataProvider get_component_from_classname_provider
425
     * @param string $classname The class name to test
426
     * @param string|null $expected The expected component
427
     * @covers \core_component::get_component_from_classname
428
     */
429
    public function test_get_component_from_classname(
430
        string $classname,
431
        string|null $expected,
432
    ): void {
433
        $this->assertEquals(
434
            $expected,
435
            \core_component::get_component_from_classname($classname),
436
        );
437
    }
438
 
439
    /**
440
     * Data provider for get_component_from_classname tests.
441
     *
442
     * @return array
443
     */
444
    public static function get_component_from_classname_provider(): array {
445
        // Start off with testcases which have the leading \.
446
        $testcases = [
447
            // Core.
448
            [\core\example::class, 'core'],
449
 
450
            // A core subsystem.
451
            [\core_message\example::class, 'core_message'],
452
 
453
            // A fake core subsystem.
454
            [\core_fake\example::class, null],
455
 
456
            // A plugin.
457
            [\mod_forum\example::class, 'mod_forum'],
458
 
459
            // A plugin in the old style is not supported.
460
            [\mod_forum_example::class, null],
461
 
462
            // A fake plugin.
463
            [\mod_fake\example::class, null],
464
 
465
            // A subplugin.
466
            [\tiny_link\example::class, 'tiny_link'],
467
        ];
468
 
469
        // Duplicate the testcases, adding a nested namespace.
470
        $testcases = array_merge(
471
            $testcases,
472
            array_map(
473
                fn ($testcase) => [$testcase[0] . '\\in\\sub\\directory', $testcase[1]],
474
                $testcases,
475
            ),
476
        );
477
 
478
        // Duplicate the testcases, removing the leading \.
479
        return array_merge(
480
            $testcases,
481
            array_map(
482
                fn ($testcase) => [ltrim($testcase[0], '\\'), $testcase[1]],
483
                $testcases,
484
            ),
485
        );
486
    }
487
 
488
    public function test_deprecated_get_component_directory(): void {
489
        $plugintypes = core_component::get_plugin_types();
490
        foreach ($plugintypes as $plugintype => $fulldir) {
491
            $plugins = core_component::get_plugin_list($plugintype);
492
            foreach ($plugins as $pluginname => $plugindir) {
493
                $this->assertSame($plugindir, get_component_directory(($plugintype . '_' . $pluginname)));
494
            }
495
        }
496
 
497
        $subsystems = core_component::get_core_subsystems();
498
        foreach ($subsystems as $subsystem => $fulldir) {
499
            $this->assertSame($fulldir, get_component_directory(('core_' . $subsystem)));
500
        }
501
    }
502
 
503
    public function test_get_subtype_parent(): void {
504
        global $CFG;
505
 
506
        $this->assertNull(core_component::get_subtype_parent('mod'));
507
 
508
        // Any plugin with more subtypes is ok here.
509
        $this->assertFileExists("$CFG->dirroot/mod/assign/db/subplugins.json");
510
        $this->assertSame('mod_assign', core_component::get_subtype_parent('assignsubmission'));
511
        $this->assertSame('mod_assign', core_component::get_subtype_parent('assignfeedback'));
512
        $this->assertNull(core_component::get_subtype_parent('assignxxxxx'));
513
    }
514
 
515
    public function test_get_subplugins(): void {
516
        global $CFG;
517
 
518
        // Any plugin with more subtypes is ok here.
519
        $this->assertFileExists("$CFG->dirroot/mod/assign/db/subplugins.json");
520
 
521
        $subplugins = core_component::get_subplugins('mod_assign');
522
        $this->assertSame(['assignsubmission', 'assignfeedback'], array_keys($subplugins));
523
 
524
        $subs = core_component::get_plugin_list('assignsubmission');
525
        $feeds = core_component::get_plugin_list('assignfeedback');
526
 
527
        $this->assertSame(array_keys($subs), $subplugins['assignsubmission']);
528
        $this->assertSame(array_keys($feeds), $subplugins['assignfeedback']);
529
 
530
        // Any plugin without subtypes is ok here.
531
        $this->assertFileExists("$CFG->dirroot/mod/choice");
532
        $this->assertFileDoesNotExist("$CFG->dirroot/mod/choice/db/subplugins.json");
533
 
534
        $this->assertNull(core_component::get_subplugins('mod_choice'));
535
 
536
        $this->assertNull(core_component::get_subplugins('xxxx_yyyy'));
537
    }
538
 
539
    public function test_get_plugin_types_with_subplugins(): void {
540
        global $CFG;
541
 
542
        $types = core_component::get_plugin_types_with_subplugins();
543
 
544
        // Hardcode it here to detect if anybody hacks the code to include more subplugin types.
545
        $expected = [
546
            'mod' => "$CFG->dirroot/mod",
547
            'editor' => "$CFG->dirroot/lib/editor",
548
            'tool' => "$CFG->dirroot/$CFG->admin/tool",
549
            'local' => "$CFG->dirroot/local",
550
        ];
551
 
552
        $this->assertSame($expected, $types);
553
    }
554
 
555
    public function test_get_plugin_list_with_file(): void {
556
        $this->resetAfterTest(true);
557
 
558
        // No extra reset here because core_component reset automatically.
559
 
560
        $expected = [];
561
        $reports = core_component::get_plugin_list('report');
562
        foreach ($reports as $name => $fulldir) {
563
            if (file_exists("$fulldir/lib.php")) {
564
                $expected[] = $name;
565
            }
566
        }
567
 
568
        // Test cold.
569
        $list = core_component::get_plugin_list_with_file('report', 'lib.php', false);
570
        $this->assertEquals($expected, array_keys($list));
571
 
572
        // Test hot.
573
        $list = core_component::get_plugin_list_with_file('report', 'lib.php', false);
574
        $this->assertEquals($expected, array_keys($list));
575
 
576
        // Test with include.
577
        $list = core_component::get_plugin_list_with_file('report', 'lib.php', true);
578
        $this->assertEquals($expected, array_keys($list));
579
 
580
        // Test missing.
581
        $list = core_component::get_plugin_list_with_file('report', 'idontexist.php', true);
582
        $this->assertEquals([], array_keys($list));
583
    }
584
 
585
    /**
586
     * Tests for get_component_classes_in_namespace.
587
     */
588
    public function test_get_component_classes_in_namespace(): void {
589
        // Unexisting.
590
        $this->assertCount(0, core_component::get_component_classes_in_namespace('core_unexistingcomponent', 'something'));
591
        $this->assertCount(0, core_component::get_component_classes_in_namespace('auth_cas', 'something'));
592
 
593
        // Matches the last namespace level name not partials.
594
        $this->assertCount(0, core_component::get_component_classes_in_namespace('auth_cas', 'tas'));
595
        $this->assertCount(0, core_component::get_component_classes_in_namespace('core_user', 'course'));
596
        $this->assertCount(0, core_component::get_component_classes_in_namespace('mod_forum', 'output\\emaildigest'));
597
        $this->assertCount(0, core_component::get_component_classes_in_namespace('mod_forum', '\\output\\emaildigest'));
598
 
599
        // Without either a component or namespace it returns an empty array.
600
        $this->assertEmpty(\core_component::get_component_classes_in_namespace());
601
        $this->assertEmpty(\core_component::get_component_classes_in_namespace(null));
602
        $this->assertEmpty(\core_component::get_component_classes_in_namespace(null, ''));
603
    }
604
 
605
    /**
606
     * Test that the get_component_classes_in_namespace() function returns classes in the correct namespace.
607
     *
608
     * @dataProvider get_component_classes_in_namespace_provider
609
     * @param array $methodargs
610
     * @param string $expectedclassnameformat
611
     */
612
    public function test_get_component_classes_in_namespace_provider(
613
        array $methodargs,
614
        string $expectedclassnameformat,
615
    ): void {
616
        $classlist = core_component::get_component_classes_in_namespace(...$methodargs);
617
        $this->assertGreaterThan(0, count($classlist));
618
 
619
        foreach (array_keys($classlist) as $classname) {
620
            $this->assertStringMatchesFormat($expectedclassnameformat, $classname);
621
        }
622
    }
623
 
624
    /**
625
     * Data provider for get_component_classes_in_namespace tests.
626
     *
627
     * @return array
628
     */
629
    public static function get_component_classes_in_namespace_provider(): array {
630
        return [
631
            // Matches the last namespace level name not partials.
632
            [
633
                ['mod_forum', 'output\\email'],
634
                'mod_forum\output\email\%s',
635
            ],
636
            [
637
                ['mod_forum', '\\output\\email'],
638
                'mod_forum\output\email\%s',
639
            ],
640
            [
641
                ['mod_forum', 'output\\email\\'],
642
                'mod_forum\output\email\%s',
643
            ],
644
            [
645
                ['mod_forum', '\\output\\email\\'],
646
                'mod_forum\output\email\%s',
647
            ],
648
            // Prefix with backslash if it doesn\'t come prefixed.
649
            [
650
                ['auth_cas', 'task'],
651
                'auth_cas\task\%s',
652
            ],
653
            [
654
                ['auth_cas', '\\task'],
655
                'auth_cas\task\%s',
656
            ],
657
 
658
            // Core as a component works, the function can normalise the component name.
659
            [
660
                ['core', 'update'],
661
                'core\update\%s',
662
            ],
663
            [
664
                ['', 'update'],
665
                'core\update\%s',
666
            ],
667
            [
668
                ['moodle', 'update'],
669
                'core\update\%s',
670
            ],
671
 
672
            // Multiple levels.
673
            [
674
                ['core_user', '\\output\\myprofile\\'],
675
                'core_user\output\myprofile\%s',
676
            ],
677
            [
678
                ['core_user', 'output\\myprofile\\'],
679
                'core_user\output\myprofile\%s',
680
            ],
681
            [
682
                ['core_user', '\\output\\myprofile'],
683
                'core_user\output\myprofile\%s',
684
            ],
685
            [
686
                ['core_user', 'output\\myprofile'],
687
                'core_user\output\myprofile\%s',
688
            ],
689
 
690
            // Without namespace it returns classes/ classes.
691
            [
692
                ['tool_mobile', ''],
693
                'tool_mobile\%s',
694
            ],
695
            [
696
                ['tool_filetypes'],
697
                'tool_filetypes\%s',
698
            ],
699
 
700
            // Multiple levels.
701
            [
702
                ['core_user', '\\output\\myprofile\\'],
703
                'core_user\output\myprofile\%s',
704
            ],
705
 
706
            // When no component is specified, classes are returned for the namespace in all components.
707
            // (We don't assert exact amounts here as the count of `output` classes will change depending on plugins installed).
708
            [
709
                ['core', 'output'],
710
                'core\%s',
711
            ],
712
            [
713
                [null, 'output'],
714
                '%s',
715
            ],
716
        ];
717
    }
718
 
719
    /**
720
     * Data provider for classloader test
721
     */
722
    public static function classloader_provider(): array {
723
        global $CFG;
724
 
725
        // As part of these tests, we Check that there are no unexpected problems with overlapping PSR namespaces.
726
        // This is not in the spec, but may come up in some libraries using both namespaces and PEAR-style class names.
727
        // If problems arise we can remove this test, but will need to add a warning.
728
        // Normalise to forward slash for testing purposes.
729
        $directory = str_replace('\\', '/', $CFG->dirroot) . "/lib/tests/fixtures/component/";
730
 
731
        $psr0 = [
732
          'psr0'      => 'lib/tests/fixtures/component/psr0',
733
          'overlap'   => 'lib/tests/fixtures/component/overlap',
734
        ];
735
        $psr4 = [
736
          'psr4'      => 'lib/tests/fixtures/component/psr4',
737
          'overlap'   => 'lib/tests/fixtures/component/overlap',
738
        ];
739
        return [
11 efrain 740
            'PSR-0 Classloading - Root' => [
741
                'psr0' => $psr0,
742
                'psr4' => $psr4,
743
                'classname' => 'psr0_main',
744
                'includedfiles' => "{$directory}psr0/main.php",
745
            ],
746
            'PSR-0 Classloading - Sub namespace - underscores' => [
747
                'psr0' => $psr0,
748
                'psr4' => $psr4,
749
                'classname' => 'psr0_subnamespace_example',
750
                'includedfiles' => "{$directory}psr0/subnamespace/example.php",
751
            ],
752
            'PSR-0 Classloading - Sub namespace - slashes' => [
753
                'psr0' => $psr0,
754
                'psr4' => $psr4,
755
                'classname' => 'psr0\\subnamespace\\slashes',
756
                'includedfiles' => "{$directory}psr0/subnamespace/slashes.php",
757
            ],
758
            'PSR-4 Classloading - Root' => [
759
                'psr0' => $psr0,
760
                'psr4' => $psr4,
761
                'classname' => 'psr4\\main',
762
                'includedfiles' => "{$directory}psr4/main.php",
763
            ],
764
            'PSR-4 Classloading - Sub namespace' => [
765
                'psr0' => $psr0,
766
                'psr4' => $psr4,
767
                'classname' => 'psr4\\subnamespace\\example',
768
                'includedfiles' => "{$directory}psr4/subnamespace/example.php",
769
            ],
770
            'PSR-4 Classloading - Ensure underscores are not converted to paths' => [
771
                'psr0' => $psr0,
772
                'psr4' => $psr4,
773
                'classname' => 'psr4\\subnamespace\\underscore_example',
774
                'includedfiles' => "{$directory}psr4/subnamespace/underscore_example.php",
775
            ],
776
            'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [
777
                'psr0' => $psr0,
778
                'psr4' => $psr4,
779
                'classname' => 'overlap\\subnamespace\\example',
780
                'includedfiles' => "{$directory}overlap/subnamespace/example.php",
781
            ],
782
            'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [
783
                'psr0' => $psr0,
784
                'psr4' => $psr4,
785
                'classname' => 'overlap_subnamespace_example2',
786
                'includedfiles' => "{$directory}overlap/subnamespace/example2.php",
787
            ],
1 efrain 788
        ];
789
    }
790
 
791
    /**
792
     * Test the classloader.
793
     *
794
     * @dataProvider classloader_provider
795
     * @param array $psr0 The PSR-0 namespaces to be used in the test.
796
     * @param array $psr4 The PSR-4 namespaces to be used in the test.
797
     * @param string $classname The name of the class to attempt to load.
798
     * @param string $includedfiles The file expected to be loaded.
799
     * @runInSeparateProcess
800
     */
801
    public function test_classloader($psr0, $psr4, $classname, $includedfiles): void {
802
        $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
803
        $psr0namespaces->setValue(null, $psr0);
804
 
805
        $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces');
806
        $psr4namespaces->setValue(null, $psr4);
807
 
808
        core_component::classloader($classname);
809
        if (DIRECTORY_SEPARATOR != '/') {
810
            // Denormalise the expected path so that we can quickly compare with get_included_files.
811
            $includedfiles = str_replace('/', DIRECTORY_SEPARATOR, $includedfiles);
812
        }
813
        $this->assertContains($includedfiles, get_included_files());
814
        $this->assertTrue(class_exists($classname, false));
815
    }
816
 
817
    /**
818
     * Data provider for psr_classloader test
819
     */
820
    public static function psr_classloader_provider(): array {
821
        global $CFG;
822
 
823
        // As part of these tests, we Check that there are no unexpected problems with overlapping PSR namespaces.
824
        // This is not in the spec, but may come up in some libraries using both namespaces and PEAR-style class names.
825
        // If problems arise we can remove this test, but will need to add a warning.
826
        // Normalise to forward slash for testing purposes.
827
        $dirroot = str_replace('\\', '/', $CFG->dirroot);
828
        $directory = "{$dirroot}/lib/tests/fixtures/component/";
829
 
830
        $psr0 = [
831
          'psr0'      => 'lib/tests/fixtures/component/psr0',
832
          'overlap'   => 'lib/tests/fixtures/component/overlap',
833
        ];
834
        $psr4 = [
835
          'psr4'      => 'lib/tests/fixtures/component/psr4',
836
          'overlap'   => 'lib/tests/fixtures/component/overlap',
837
        ];
838
        return [
11 efrain 839
            'PSR-0 Classloading - Root' => [
840
                'psr0' => $psr0,
841
                'psr4' => $psr4,
842
                'classname' => 'psr0_main',
843
                'file' => "{$directory}psr0/main.php",
844
            ],
845
            'PSR-0 Classloading - Sub namespace - underscores' => [
846
                'psr0' => $psr0,
847
                'psr4' => $psr4,
848
                'classname' => 'psr0_subnamespace_example',
849
                'file' => "{$directory}psr0/subnamespace/example.php",
850
            ],
851
            'PSR-0 Classloading - Sub namespace - slashes' => [
852
                'psr0' => $psr0,
853
                'psr4' => $psr4,
854
                'classname' => 'psr0\\subnamespace\\slashes',
855
                'file' => "{$directory}psr0/subnamespace/slashes.php",
856
            ],
857
            'PSR-0 Classloading - non-existent file' => [
858
                'psr0' => $psr0,
859
                'psr4' => $psr4,
860
                'classname' => 'psr0_subnamespace_nonexistent_file',
861
                'file' => false,
862
            ],
863
            'PSR-4 Classloading - Root' => [
864
                'psr0' => $psr0,
865
                'psr4' => $psr4,
866
                'classname' => 'psr4\\main',
867
                'file' => "{$directory}psr4/main.php",
868
            ],
869
            'PSR-4 Classloading - Sub namespace' => [
870
                'psr0' => $psr0,
871
                'psr4' => $psr4,
872
                'classname' => 'psr4\\subnamespace\\example',
873
                'file' => "{$directory}psr4/subnamespace/example.php",
874
            ],
875
            'PSR-4 Classloading - Ensure underscores are not converted to paths' => [
876
                'psr0' => $psr0,
877
                'psr4' => $psr4,
878
                'classname' => 'psr4\\subnamespace\\underscore_example',
879
                'file' => "{$directory}psr4/subnamespace/underscore_example.php",
880
            ],
881
            'PSR-4 Classloading - non-existent file' => [
882
                'psr0' => $psr0,
883
                'psr4' => $psr4,
884
                'classname' => 'psr4\\subnamespace\\nonexistent',
885
                'file' => false,
886
            ],
887
            'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [
888
                'psr0' => $psr0,
889
                'psr4' => $psr4,
890
                'classname' => 'overlap\\subnamespace\\example',
891
                'file' => "{$directory}overlap/subnamespace/example.php",
892
            ],
893
            'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [
894
                'psr0' => $psr0,
895
                'psr4' => $psr4,
896
                'classname' => 'overlap_subnamespace_example2',
897
                'file' => "{$directory}overlap/subnamespace/example2.php",
898
            ],
1 efrain 899
            'PSR-4 namespaces can come from multiple sources - first source' => [
900
                'psr0' => $psr0,
901
                'psr4' => [
902
                    'Psr\\Http\\Message' => [
903
                        'lib/psr/http-message/src',
904
                        'lib/psr/http-factory/src',
905
                    ],
906
                ],
907
                'classname' => 'Psr\Http\Message\ServerRequestInterface',
908
                'includedfiles' => "{$dirroot}/lib/psr/http-message/src/ServerRequestInterface.php",
909
            ],
910
            'PSR-4 namespaces can come from multiple sources - second source' => [
911
                'psr0' => [],
912
                'psr4' => [
913
                    'Psr\\Http\\Message' => [
914
                        'lib/psr/http-message/src',
915
                        'lib/psr/http-factory/src',
916
                    ],
917
                ],
918
                'classname' => 'Psr\Http\Message\ServerRequestFactoryInterface',
919
                'includedfiles' => "{$dirroot}/lib/psr/http-factory/src/ServerRequestFactoryInterface.php",
920
            ],
921
        ];
922
    }
923
 
924
    /**
11 efrain 925
     * Test that the classloader can load from the test namespaces.
926
     */
927
    public function test_classloader_tests_namespace(): void {
928
        global $CFG;
929
 
930
        $this->resetAfterTest();
931
 
932
        $getclassfilecontent = function (string $classname, ?string $namespace): string {
933
            if ($namespace) {
934
                $content = "<?php\nnamespace $namespace;\nclass $classname {}";
935
            } else {
936
                $content = "<?php\nclass $classname {}";
937
            }
938
            return $content;
939
        };
940
 
941
        $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, [
942
            'lib' => [
943
                'classes' => [
944
                    'example.php' => $getclassfilecontent('example', 'core'),
945
                ],
946
                'tests' => [
947
                    'classes' => [
948
                        'example_classname.php' => $getclassfilecontent('example_classname', \core\tests::class),
949
                    ],
950
                    'behat' => [
951
                        'example_classname.php' => $getclassfilecontent('example_classname', \core\behat::class),
952
                    ],
953
                ],
954
            ],
955
        ]);
956
 
957
        // Note: This is pretty hacky, but it's the only way to test the classloader.
958
        // We have to override the dirroot and libdir, and then reset the plugintypes property.
959
        $CFG->dirroot = $vfileroot->url();
960
        $CFG->libdir = $vfileroot->url() . '/lib';
961
        \core_component::reset();
962
 
963
        // Existing classes do not break.
964
        $this->assertTrue(
965
            class_exists(\core\example::class),
966
        );
967
 
968
        // Test and behat classes work.
969
        $this->assertTrue(
970
            class_exists(\core\tests\example_classname::class),
971
        );
972
        $this->assertTrue(
973
            class_exists(\core\behat\example_classname::class),
974
        );
975
 
976
        // Non-existent classes do not do anything.
977
        $this->assertFalse(
978
            class_exists(\core\tests\example_classname_not_found::class),
979
        );
980
    }
981
 
982
    /**
1 efrain 983
     * Test the PSR classloader.
984
     *
985
     * @dataProvider psr_classloader_provider
986
     * @param array $psr0 The PSR-0 namespaces to be used in the test.
987
     * @param array $psr4 The PSR-4 namespaces to be used in the test.
988
     * @param string $classname The name of the class to attempt to load.
989
     * @param string|bool $file The expected file corresponding to the class or false for nonexistant.
990
     * @runInSeparateProcess
991
     */
992
    public function test_psr_classloader($psr0, $psr4, $classname, $file): void {
993
        $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
994
        $psr0namespaces->setValue(null, $psr0);
995
 
996
        $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces');
997
        $psr4namespaces->setValue(null, $psr4);
998
 
999
        $component = new ReflectionClass('core_component');
1000
        $psrclassloader = $component->getMethod('psr_classloader');
1001
 
1002
        $returnvalue = $psrclassloader->invokeArgs(null, [$classname]);
1003
        // Normalise to forward slashes for testing comparison.
1004
        if ($returnvalue) {
1005
            $returnvalue = str_replace('\\', '/', $returnvalue);
1006
        }
1007
        $this->assertEquals($file, $returnvalue);
1008
    }
1009
 
1010
    /**
1011
     * Data provider for get_class_file test
1012
     */
1013
    public static function get_class_file_provider(): array {
1014
        global $CFG;
1015
 
1016
        return [
1017
          'Getting a file with underscores' => [
1018
              'classname' => 'Test_With_Underscores',
1019
              'prefix' => "Test",
1020
              'path' => 'test/src',
1021
              'separators' => ['_'],
1022
              'result' => $CFG->dirroot . "/test/src/With/Underscores.php",
1023
          ],
1024
          'Getting a file with slashes' => [
1025
              'classname' => 'Test\\With\\Slashes',
1026
              'prefix' => "Test",
1027
              'path' => 'test/src',
1028
              'separators' => ['\\'],
1029
              'result' => $CFG->dirroot . "/test/src/With/Slashes.php",
1030
          ],
1031
          'Getting a file with multiple namespaces' => [
1032
              'classname' => 'Test\\With\\Multiple\\Namespaces',
1033
              'prefix' => "Test\\With",
1034
              'path' => 'test/src',
1035
              'separators' => ['\\'],
1036
              'result' => $CFG->dirroot . "/test/src/Multiple/Namespaces.php",
1037
          ],
1038
          'Getting a file with multiple namespaces (non-existent)' => [
1039
              'classname' => 'Nonexistent\\Namespace\\Test',
1040
              'prefix' => "Test",
1041
              'path' => 'test/src',
1042
              'separators' => ['\\'],
1043
              'result' => false,
1044
          ],
1045
        ];
1046
    }
1047
 
1048
    /**
1049
     * Test the PSR classloader.
1050
     *
1051
     * @dataProvider get_class_file_provider
1052
     * @param string $classname the name of the class.
1053
     * @param string $prefix The namespace prefix used to identify the base directory of the source files.
1054
     * @param string $path The relative path to the base directory of the source files.
1055
     * @param string[] $separators The characters that should be used for separating.
1056
     * @param string|bool $result The expected result to be returned from get_class_file.
1057
     */
1058
    public function test_get_class_file($classname, $prefix, $path, $separators, $result): void {
1059
        $component = new ReflectionClass('core_component');
1060
        $psrclassloader = $component->getMethod('get_class_file');
1061
 
1062
        $file = $psrclassloader->invokeArgs(null, [$classname, $prefix, $path, $separators]);
1063
        $this->assertEquals($result, $file);
1064
    }
1065
 
1066
    /**
1067
     * Confirm the get_component_list method contains an entry for every component.
1068
     */
1069
    public function test_get_component_list_contains_all_components(): void {
1070
        global $CFG;
1071
        $componentslist = \core_component::get_component_list();
1072
 
1073
        // We should have an entry for each plugin type, and one additional for 'core'.
1074
        $plugintypes = \core_component::get_plugin_types();
1075
        $numelementsexpected = count($plugintypes) + 1;
1076
        $this->assertEquals($numelementsexpected, count($componentslist));
1077
 
1078
        // And an entry for each of the plugin types.
1079
        foreach (array_keys($plugintypes) as $plugintype) {
1080
            $this->assertArrayHasKey($plugintype, $componentslist);
1081
        }
1082
 
1083
        // And one for 'core'.
1084
        $this->assertArrayHasKey('core', $componentslist);
1085
 
1086
        // Check a few of the known plugin types to confirm their presence at their respective type index.
1087
        $this->assertEquals($componentslist['core']['core_comment'], $CFG->dirroot . '/comment');
1088
        $this->assertEquals($componentslist['mod']['mod_forum'], $CFG->dirroot . '/mod/forum');
1089
        $this->assertEquals($componentslist['tool']['tool_usertours'], $CFG->dirroot . '/' . $CFG->admin . '/tool/usertours');
1090
    }
1091
 
1092
    /**
1093
     * Test the get_component_names() method.
1094
     *
1095
     * @dataProvider get_component_names_provider
1096
     * @param bool $includecore Whether to include core in the list.
1097
     * @param bool $coreexpected Whether core is expected to be in the list.
1098
     */
1099
    public function test_get_component_names(
1100
        bool $includecore,
1101
        bool $coreexpected,
1102
    ): void {
1103
        global $CFG;
1104
        $componentnames = \core_component::get_component_names($includecore);
1105
 
1106
        // We should have an entry for each plugin type.
1107
        $plugintypes = \core_component::get_plugin_types();
1108
        $numplugintypes = 0;
1109
        foreach (array_keys($plugintypes) as $type) {
1110
            $numplugintypes += count(\core_component::get_plugin_list($type));
1111
        }
1112
        // And an entry for each core subsystem.
1113
        $numcomponents = $numplugintypes + count(\core_component::get_core_subsystems());
1114
 
1115
        if ($coreexpected) {
1116
            // Add one for core.
1117
            $numcomponents++;
1118
        }
1119
        $this->assertEquals($numcomponents, count($componentnames));
1120
 
1121
        // Check a few of the known plugin types to confirm their presence at their respective type index.
1122
        $this->assertContains('core_comment', $componentnames);
1123
        $this->assertContains('mod_forum', $componentnames);
1124
        $this->assertContains('tool_usertours', $componentnames);
1125
        $this->assertContains('core_favourites', $componentnames);
1126
        if ($coreexpected) {
1127
            $this->assertContains('core', $componentnames);
1128
        } else {
1129
            $this->assertNotContains('core', $componentnames);
1130
        }
1131
    }
1132
 
1133
    /**
1134
     * Data provider for get_component_names() test.
1135
     *
1136
     * @return array
1137
     */
1138
    public static function get_component_names_provider(): array {
1139
        return [
1140
            [false, false],
1141
            [true, true],
1142
        ];
1143
    }
1144
 
1145
    /**
1146
     * Basic tests for APIs related functions in the core_component class.
1147
     */
1148
    public function test_apis_methods(): void {
1149
        $apis = core_component::get_core_apis();
1150
        $this->assertIsArray($apis);
1151
 
1152
        $apinames = core_component::get_core_api_names();
1153
        $this->assertIsArray($apis);
1154
 
1155
        // Both should return the very same APIs.
1156
        $this->assertEquals($apinames, array_keys($apis));
1157
 
1158
        $this->assertFalse(core_component::is_core_api('lalala'));
1159
        $this->assertTrue(core_component::is_core_api('privacy'));
1160
    }
1161
 
1162
    /**
1163
     * Test that the apis.json structure matches expectations
1164
     *
1165
     * While we include an apis.schema.json file in core, there isn't any PHP built-in allowing us
1166
     * to validate it (3rd part libraries needed). Plus the schema doesn't allow to validate things
1167
     * like uniqueness or sorting. We are going to do all that here.
1168
     */
1169
    public function test_apis_json_validation(): void {
1170
        $apis = $sortedapis = core_component::get_core_apis();
1171
        ksort($sortedapis); // We'll need this later.
1172
 
1173
        $subsystems = core_component::get_core_subsystems(); // To verify all apis are pointing to valid subsystems.
1174
        $subsystems['core'] = 'anything'; // Let's add 'core' because it's a valid component for apis.
1175
 
1176
        // General structure validations.
1177
        $this->assertIsArray($apis);
1178
        $this->assertGreaterThan(25, count($apis));
1179
        $this->assertArrayHasKey('privacy', $apis); // Verify a few.
1180
        $this->assertArrayHasKey('external', $apis);
1181
        $this->assertArrayHasKey('search', $apis);
1182
        $this->assertEquals(array_keys($sortedapis), array_keys($apis)); // Verify json is sorted alphabetically.
1183
 
1184
        // Iterate over all apis and perform more validations.
1185
        foreach ($apis as $apiname => $attributes) {
1186
            // Message, to be used later and easier finding the problem.
1187
            $message = "Validation problem found with API: {$apiname}";
1188
 
1189
            $this->assertIsObject($attributes, $message);
1190
            $this->assertMatchesRegularExpression('/^[a-z][a-z0-9]+$/', $apiname, $message);
1191
            $this->assertEquals(['component', 'allowedlevel2', 'allowedspread'], array_keys((array)$attributes), $message);
1192
 
1193
            // Verify attributes.
1194
            if ($apiname !== 'core') { // Exception for core api, it doesn't have component.
1195
                // Check that component attribute looks correct.
1196
                $this->assertMatchesRegularExpression('/^(core|[a-z][a-z0-9_]+)$/', $attributes->component, $message);
1197
                // Ensure that the api component (without the core_ prefix) is a correct subsystem.
1198
                $this->assertArrayHasKey(str_replace('core_', '', $attributes->component), $subsystems, $message);
1199
            } else {
1200
                $this->assertNull($attributes->component, $message);
1201
            }
1202
 
1203
 
1204
            // Now check for the rest of attributes.
1205
            $this->assertIsBool($attributes->allowedlevel2, $message);
1206
            $this->assertIsBool($attributes->allowedspread, $message);
1207
 
1208
            // Cannot spread if level2 is not allowed.
1209
            $this->assertLessThanOrEqual($attributes->allowedlevel2, $attributes->allowedspread, $message);
1210
        }
1211
    }
1212
 
1213
    /**
1214
     * Test for monologo icons check in plugins.
1215
     */
1216
    public function test_has_monologo_icon(): void {
1217
        // The Forum activity plugin has monologo icons.
1218
        $this->assertTrue(core_component::has_monologo_icon('mod', 'forum'));
1219
        // The core H5P subsystem doesn't have monologo icons.
1220
        $this->assertFalse(core_component::has_monologo_icon('core', 'h5p'));
1221
        // The function will return false for a non-existent component.
1222
        $this->assertFalse(core_component::has_monologo_icon('randomcomponent', 'h5p'));
1223
    }
1224
 
1225
    /*
1226
     * Tests the getter for the db directory summary hash.
1227
     *
1228
     * @covers \core_component::get_all_directory_hashes
1229
     */
1230
    public function test_get_db_directories_hash(): void {
1231
        $initial = \core_component::get_all_component_hash();
1232
 
1233
        $dir = make_request_directory();
1234
        $hashes = \core_component::get_all_directory_hashes([$dir]);
1235
        $emptydirhash = \core_component::get_all_component_hash([$hashes]);
1236
 
1237
        // Confirm that a single empty directory is a different hash to the core hash.
1238
        $this->assertNotEquals($initial, $emptydirhash);
1239
 
1240
        // Now lets add something to the dir, and check the hash is different.
1241
        $file = fopen($dir . '/test.php', 'w');
1242
        fwrite($file, 'sometestdata');
1243
        fclose($file);
1244
 
1245
        $hashes = \core_component::get_all_directory_hashes([$dir]);
1246
        $onefiledirhash = \core_component::get_all_component_hash([$hashes]);
1247
        $this->assertNotEquals($emptydirhash, $onefiledirhash);
1248
 
1249
        // Now add a subdirectory inside the request dir. This should not affect the hash.
1250
        mkdir($dir . '/subdir');
1251
        $hashes = \core_component::get_all_directory_hashes([$dir]);
1252
        $finalhash = \core_component::get_all_component_hash([$hashes]);
1253
        $this->assertEquals($onefiledirhash, $finalhash);
1254
    }
1255
}