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/**
18 * core_component related tests.
19 *
20 * @package    core
21 * @category   phpunit
22 * @copyright  2013 Petr Skoda {@link http://skodak.org}
23 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26defined('MOODLE_INTERNAL') || die();
27
28
29/**
30 * Class core_component_testcase.
31 */
32class core_component_testcase extends advanced_testcase {
33
34    /**
35     * To be changed if number of subsystems increases/decreases,
36     * this is defined here to annoy devs that try to add more without any thinking,
37     * always verify that it does not collide with any existing add-on modules and subplugins!!!
38     */
39    const SUBSYSTEMCOUNT = 71;
40
41    public function setUp() {
42        $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
43        $psr0namespaces->setAccessible(true);
44        $this->oldpsr0namespaces = $psr0namespaces->getValue(null);
45
46        $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces');
47        $psr4namespaces->setAccessible(true);
48        $this->oldpsr4namespaces = $psr4namespaces->getValue(null);
49    }
50    public function tearDown() {
51        $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
52        $psr0namespaces->setAccessible(true);
53        $psr0namespaces->setValue(null, $this->oldpsr0namespaces);
54
55        $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces');
56        $psr4namespaces->setAccessible(true);
57        $psr4namespaces->setValue(null, $this->oldpsr4namespaces);
58    }
59
60    public function test_get_core_subsystems() {
61        global $CFG;
62
63        $subsystems = core_component::get_core_subsystems();
64
65        $this->assertCount(self::SUBSYSTEMCOUNT, $subsystems, 'Oh, somebody added or removed a core subsystem, think twice before doing that!');
66
67        // Make sure all paths are full/null, exist and are inside dirroot.
68        foreach ($subsystems as $subsystem => $fulldir) {
69            $this->assertFalse(strpos($subsystem, '_'), 'Core subsystems must be one work without underscores');
70            if ($fulldir === null) {
71                if ($subsystem === 'filepicker' or $subsystem === 'help') {
72                    // Arrgghh, let's not introduce more subsystems for no real reason...
73                } else {
74                    // Lang strings.
75                    $this->assertFileExists("$CFG->dirroot/lang/en/$subsystem.php", 'Core subsystems without fulldir are usually used for lang strings.');
76                }
77                continue;
78            }
79            $this->assertFileExists($fulldir);
80            // Check that base uses realpath() separators and "/" in the subdirs.
81            $this->assertStringStartsWith($CFG->dirroot.'/', $fulldir);
82            $reldir = substr($fulldir, strlen($CFG->dirroot)+1);
83            $this->assertFalse(strpos($reldir, '\\'));
84        }
85
86        // Make sure all core language files are also subsystems!
87        $items = new DirectoryIterator("$CFG->dirroot/lang/en");
88        foreach ($items as $item) {
89            if ($item->isDot() or $item->isDir()) {
90                continue;
91            }
92            $file = $item->getFilename();
93            if ($file === 'moodle.php') {
94                // Do not add new lang strings unless really necessary!!!
95                continue;
96            }
97
98            if (substr($file, -4) !== '.php') {
99                continue;
100            }
101            $file = substr($file, 0, strlen($file)-4);
102            $this->assertArrayHasKey($file, $subsystems, 'All core lang files should be subsystems, think twice before adding anything!');
103        }
104        unset($item);
105        unset($items);
106
107    }
108
109    public function test_deprecated_get_core_subsystems() {
110        global $CFG;
111
112        $subsystems = core_component::get_core_subsystems();
113
114        $this->assertSame($subsystems, get_core_subsystems(true));
115
116        $realsubsystems = get_core_subsystems();
117        $this->assertDebuggingCalled();
118        $this->assertSame($realsubsystems, get_core_subsystems(false));
119        $this->assertDebuggingCalled();
120
121        $this->assertEquals(count($subsystems), count($realsubsystems));
122
123        foreach ($subsystems as $subsystem => $fulldir) {
124            $this->assertArrayHasKey($subsystem, $realsubsystems);
125            if ($fulldir === null) {
126                $this->assertNull($realsubsystems[$subsystem]);
127                continue;
128            }
129            $this->assertSame($fulldir, $CFG->dirroot.'/'.$realsubsystems[$subsystem]);
130        }
131    }
132
133    public function test_get_plugin_types() {
134        global $CFG;
135
136        $this->assertTrue(empty($CFG->themedir), 'Non-empty $CFG->themedir is not covered by any tests yet, you need to disable it.');
137
138        $plugintypes = core_component::get_plugin_types();
139
140        foreach ($plugintypes as $plugintype => $fulldir) {
141            $this->assertStringStartsWith("$CFG->dirroot/", $fulldir);
142        }
143    }
144
145    public function test_deprecated_get_plugin_types() {
146        global $CFG;
147
148        $plugintypes = core_component::get_plugin_types();
149
150        $this->assertSame($plugintypes, get_plugin_types());
151        $this->assertSame($plugintypes, get_plugin_types(true));
152
153        $realplugintypes = get_plugin_types(false);
154        $this->assertDebuggingCalled();
155
156        foreach ($plugintypes as $plugintype => $fulldir) {
157            $this->assertSame($fulldir, $CFG->dirroot.'/'.$realplugintypes[$plugintype]);
158        }
159    }
160
161    public function test_get_plugin_list() {
162        global $CFG;
163
164        $plugintypes = core_component::get_plugin_types();
165
166        foreach ($plugintypes as $plugintype => $fulldir) {
167            $plugins = core_component::get_plugin_list($plugintype);
168            foreach ($plugins as $pluginname => $plugindir) {
169                $this->assertStringStartsWith("$CFG->dirroot/", $plugindir);
170            }
171            if ($plugintype !== 'auth') {
172                // Let's crosscheck it with independent implementation (auth/db is an exception).
173                $reldir = substr($fulldir, strlen($CFG->dirroot)+1);
174                $dirs = get_list_of_plugins($reldir);
175                $dirs = array_values($dirs);
176                $this->assertDebuggingCalled();
177                $this->assertSame($dirs, array_keys($plugins));
178            }
179        }
180    }
181
182    public function test_deprecated_get_plugin_list() {
183        $plugintypes = core_component::get_plugin_types();
184
185        foreach ($plugintypes as $plugintype => $fulldir) {
186            $plugins = core_component::get_plugin_list($plugintype);
187            $this->assertSame($plugins, get_plugin_list($plugintype));
188        }
189    }
190
191    public function test_get_plugin_directory() {
192        $plugintypes = core_component::get_plugin_types();
193
194        foreach ($plugintypes as $plugintype => $fulldir) {
195            $plugins = core_component::get_plugin_list($plugintype);
196            foreach ($plugins as $pluginname => $plugindir) {
197                $this->assertSame($plugindir, core_component::get_plugin_directory($plugintype, $pluginname));
198            }
199        }
200    }
201
202    public function test_deprecated_get_plugin_directory() {
203        $plugintypes = core_component::get_plugin_types();
204
205        foreach ($plugintypes as $plugintype => $fulldir) {
206            $plugins = core_component::get_plugin_list($plugintype);
207            foreach ($plugins as $pluginname => $plugindir) {
208                $this->assertSame(core_component::get_plugin_directory($plugintype, $pluginname), get_plugin_directory($plugintype, $pluginname));
209            }
210        }
211    }
212
213    public function test_get_subsystem_directory() {
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    public function test_is_valid_plugin_name() {
221        $this->assertTrue(core_component::is_valid_plugin_name('mod', 'example1'));
222        $this->assertTrue(core_component::is_valid_plugin_name('mod', 'feedback360'));
223        $this->assertFalse(core_component::is_valid_plugin_name('mod', 'feedback_360'));
224        $this->assertFalse(core_component::is_valid_plugin_name('mod', '2feedback'));
225        $this->assertFalse(core_component::is_valid_plugin_name('mod', '1example'));
226        $this->assertFalse(core_component::is_valid_plugin_name('mod', 'example.xx'));
227        $this->assertFalse(core_component::is_valid_plugin_name('mod', '.example'));
228        $this->assertFalse(core_component::is_valid_plugin_name('mod', '_example'));
229        $this->assertFalse(core_component::is_valid_plugin_name('mod', 'example_'));
230        $this->assertFalse(core_component::is_valid_plugin_name('mod', 'example_x1'));
231        $this->assertFalse(core_component::is_valid_plugin_name('mod', 'example-x1'));
232        $this->assertFalse(core_component::is_valid_plugin_name('mod', 'role'));
233
234        $this->assertTrue(core_component::is_valid_plugin_name('tool', 'example1'));
235        $this->assertTrue(core_component::is_valid_plugin_name('tool', 'example_x1'));
236        $this->assertTrue(core_component::is_valid_plugin_name('tool', 'example_x1_xxx'));
237        $this->assertTrue(core_component::is_valid_plugin_name('tool', 'feedback360'));
238        $this->assertTrue(core_component::is_valid_plugin_name('tool', 'feed_back360'));
239        $this->assertTrue(core_component::is_valid_plugin_name('tool', 'role'));
240        $this->assertFalse(core_component::is_valid_plugin_name('tool', '1example'));
241        $this->assertFalse(core_component::is_valid_plugin_name('tool', 'example.xx'));
242        $this->assertFalse(core_component::is_valid_plugin_name('tool', 'example-xx'));
243        $this->assertFalse(core_component::is_valid_plugin_name('tool', '.example'));
244        $this->assertFalse(core_component::is_valid_plugin_name('tool', '_example'));
245        $this->assertFalse(core_component::is_valid_plugin_name('tool', 'example_'));
246        $this->assertFalse(core_component::is_valid_plugin_name('tool', 'example__x1'));
247    }
248
249    public function test_normalize_componentname() {
250        // Moodle core.
251        $this->assertSame('core', core_component::normalize_componentname('core'));
252        $this->assertSame('core', core_component::normalize_componentname('moodle'));
253        $this->assertSame('core', core_component::normalize_componentname(''));
254
255        // Moodle core subsystems.
256        $this->assertSame('core_admin', core_component::normalize_componentname('admin'));
257        $this->assertSame('core_admin', core_component::normalize_componentname('core_admin'));
258        $this->assertSame('core_admin', core_component::normalize_componentname('moodle_admin'));
259
260        // Activity modules and their subplugins.
261        $this->assertSame('mod_workshop', core_component::normalize_componentname('workshop'));
262        $this->assertSame('mod_workshop', core_component::normalize_componentname('mod_workshop'));
263        $this->assertSame('workshopform_accumulative', core_component::normalize_componentname('workshopform_accumulative'));
264        $this->assertSame('mod_quiz', core_component::normalize_componentname('quiz'));
265        $this->assertSame('quiz_grading', core_component::normalize_componentname('quiz_grading'));
266        $this->assertSame('mod_data', core_component::normalize_componentname('data'));
267        $this->assertSame('datafield_checkbox', core_component::normalize_componentname('datafield_checkbox'));
268
269        // Other plugin types.
270        $this->assertSame('auth_mnet', core_component::normalize_componentname('auth_mnet'));
271        $this->assertSame('enrol_self', core_component::normalize_componentname('enrol_self'));
272        $this->assertSame('block_html', core_component::normalize_componentname('block_html'));
273        $this->assertSame('block_mnet_hosts', core_component::normalize_componentname('block_mnet_hosts'));
274        $this->assertSame('local_amos', core_component::normalize_componentname('local_amos'));
275        $this->assertSame('local_admin', core_component::normalize_componentname('local_admin'));
276
277        // Unknown words without underscore are supposed to be activity modules.
278        $this->assertSame('mod_whoonearthwouldcomewithsuchastupidnameofcomponent',
279            core_component::normalize_componentname('whoonearthwouldcomewithsuchastupidnameofcomponent'));
280        // Module names can not contain underscores, this must be a subplugin.
281        $this->assertSame('whoonearth_wouldcomewithsuchastupidnameofcomponent',
282            core_component::normalize_componentname('whoonearth_wouldcomewithsuchastupidnameofcomponent'));
283        $this->assertSame('whoonearth_would_come_withsuchastupidnameofcomponent',
284            core_component::normalize_componentname('whoonearth_would_come_withsuchastupidnameofcomponent'));
285    }
286
287    public function test_normalize_component() {
288        // Moodle core.
289        $this->assertSame(array('core', null), core_component::normalize_component('core'));
290        $this->assertSame(array('core', null), core_component::normalize_component('moodle'));
291        $this->assertSame(array('core', null), core_component::normalize_component(''));
292
293        // Moodle core subsystems.
294        $this->assertSame(array('core', 'admin'), core_component::normalize_component('admin'));
295        $this->assertSame(array('core', 'admin'), core_component::normalize_component('core_admin'));
296        $this->assertSame(array('core', 'admin'), core_component::normalize_component('moodle_admin'));
297
298        // Activity modules and their subplugins.
299        $this->assertSame(array('mod', 'workshop'), core_component::normalize_component('workshop'));
300        $this->assertSame(array('mod', 'workshop'), core_component::normalize_component('mod_workshop'));
301        $this->assertSame(array('workshopform', 'accumulative'), core_component::normalize_component('workshopform_accumulative'));
302        $this->assertSame(array('mod', 'quiz'), core_component::normalize_component('quiz'));
303        $this->assertSame(array('quiz', 'grading'), core_component::normalize_component('quiz_grading'));
304        $this->assertSame(array('mod', 'data'), core_component::normalize_component('data'));
305        $this->assertSame(array('datafield', 'checkbox'), core_component::normalize_component('datafield_checkbox'));
306
307        // Other plugin types.
308        $this->assertSame(array('auth', 'mnet'), core_component::normalize_component('auth_mnet'));
309        $this->assertSame(array('enrol', 'self'), core_component::normalize_component('enrol_self'));
310        $this->assertSame(array('block', 'html'), core_component::normalize_component('block_html'));
311        $this->assertSame(array('block', 'mnet_hosts'), core_component::normalize_component('block_mnet_hosts'));
312        $this->assertSame(array('local', 'amos'), core_component::normalize_component('local_amos'));
313        $this->assertSame(array('local', 'admin'), core_component::normalize_component('local_admin'));
314
315        // Unknown words without underscore are supposed to be activity modules.
316        $this->assertSame(array('mod', 'whoonearthwouldcomewithsuchastupidnameofcomponent'),
317            core_component::normalize_component('whoonearthwouldcomewithsuchastupidnameofcomponent'));
318        // Module names can not contain underscores, this must be a subplugin.
319        $this->assertSame(array('whoonearth', 'wouldcomewithsuchastupidnameofcomponent'),
320            core_component::normalize_component('whoonearth_wouldcomewithsuchastupidnameofcomponent'));
321        $this->assertSame(array('whoonearth', 'would_come_withsuchastupidnameofcomponent'),
322            core_component::normalize_component('whoonearth_would_come_withsuchastupidnameofcomponent'));
323    }
324
325    public function test_deprecated_normalize_component() {
326        // Moodle core.
327        $this->assertSame(array('core', null), normalize_component('core'));
328        $this->assertSame(array('core', null), normalize_component(''));
329        $this->assertSame(array('core', null), normalize_component('moodle'));
330
331        // Moodle core subsystems.
332        $this->assertSame(array('core', 'admin'), normalize_component('admin'));
333        $this->assertSame(array('core', 'admin'), normalize_component('core_admin'));
334        $this->assertSame(array('core', 'admin'), normalize_component('moodle_admin'));
335
336        // Activity modules and their subplugins.
337        $this->assertSame(array('mod', 'workshop'), normalize_component('workshop'));
338        $this->assertSame(array('mod', 'workshop'), normalize_component('mod_workshop'));
339        $this->assertSame(array('workshopform', 'accumulative'), normalize_component('workshopform_accumulative'));
340        $this->assertSame(array('mod', 'quiz'), normalize_component('quiz'));
341        $this->assertSame(array('quiz', 'grading'), normalize_component('quiz_grading'));
342        $this->assertSame(array('mod', 'data'), normalize_component('data'));
343        $this->assertSame(array('datafield', 'checkbox'), normalize_component('datafield_checkbox'));
344
345        // Other plugin types.
346        $this->assertSame(array('auth', 'mnet'), normalize_component('auth_mnet'));
347        $this->assertSame(array('enrol', 'self'), normalize_component('enrol_self'));
348        $this->assertSame(array('block', 'html'), normalize_component('block_html'));
349        $this->assertSame(array('block', 'mnet_hosts'), normalize_component('block_mnet_hosts'));
350        $this->assertSame(array('local', 'amos'), normalize_component('local_amos'));
351        $this->assertSame(array('local', 'admin'), normalize_component('local_admin'));
352
353        // Unknown words without underscore are supposed to be activity modules.
354        $this->assertSame(array('mod', 'whoonearthwouldcomewithsuchastupidnameofcomponent'),
355            normalize_component('whoonearthwouldcomewithsuchastupidnameofcomponent'));
356        // Module names can not contain underscores, this must be a subplugin.
357        $this->assertSame(array('whoonearth', 'wouldcomewithsuchastupidnameofcomponent'),
358            normalize_component('whoonearth_wouldcomewithsuchastupidnameofcomponent'));
359        $this->assertSame(array('whoonearth', 'would_come_withsuchastupidnameofcomponent'),
360            normalize_component('whoonearth_would_come_withsuchastupidnameofcomponent'));
361    }
362
363    public function test_get_component_directory() {
364        $plugintypes = core_component::get_plugin_types();
365        foreach ($plugintypes as $plugintype => $fulldir) {
366            $plugins = core_component::get_plugin_list($plugintype);
367            foreach ($plugins as $pluginname => $plugindir) {
368                $this->assertSame($plugindir, core_component::get_component_directory(($plugintype.'_'.$pluginname)));
369            }
370        }
371
372        $subsystems = core_component::get_core_subsystems();
373        foreach ($subsystems as $subsystem => $fulldir) {
374            $this->assertSame($fulldir, core_component::get_component_directory(('core_'.$subsystem)));
375        }
376    }
377
378    public function test_deprecated_get_component_directory() {
379        $plugintypes = core_component::get_plugin_types();
380        foreach ($plugintypes as $plugintype => $fulldir) {
381            $plugins = core_component::get_plugin_list($plugintype);
382            foreach ($plugins as $pluginname => $plugindir) {
383                $this->assertSame($plugindir, get_component_directory(($plugintype.'_'.$pluginname)));
384            }
385        }
386
387        $subsystems = core_component::get_core_subsystems();
388        foreach ($subsystems as $subsystem => $fulldir) {
389            $this->assertSame($fulldir, get_component_directory(('core_'.$subsystem)));
390        }
391    }
392
393    public function test_get_subtype_parent() {
394        global $CFG;
395
396        $this->assertNull(core_component::get_subtype_parent('mod'));
397
398        // Any plugin with more subtypes is ok here.
399        $this->assertFileExists("$CFG->dirroot/mod/assign/db/subplugins.json");
400        $this->assertSame('mod_assign', core_component::get_subtype_parent('assignsubmission'));
401        $this->assertSame('mod_assign', core_component::get_subtype_parent('assignfeedback'));
402        $this->assertNull(core_component::get_subtype_parent('assignxxxxx'));
403    }
404
405    public function test_get_subplugins() {
406        global $CFG;
407
408        // Any plugin with more subtypes is ok here.
409        $this->assertFileExists("$CFG->dirroot/mod/assign/db/subplugins.json");
410
411        $subplugins = core_component::get_subplugins('mod_assign');
412        $this->assertSame(array('assignsubmission', 'assignfeedback'), array_keys($subplugins));
413
414        $subs = core_component::get_plugin_list('assignsubmission');
415        $feeds = core_component::get_plugin_list('assignfeedback');
416
417        $this->assertSame(array_keys($subs), $subplugins['assignsubmission']);
418        $this->assertSame(array_keys($feeds), $subplugins['assignfeedback']);
419
420        // Any plugin without subtypes is ok here.
421        $this->assertFileExists("$CFG->dirroot/mod/choice");
422        $this->assertFileNotExists("$CFG->dirroot/mod/choice/db/subplugins.json");
423
424        $this->assertNull(core_component::get_subplugins('mod_choice'));
425
426        $this->assertNull(core_component::get_subplugins('xxxx_yyyy'));
427    }
428
429    public function test_get_plugin_types_with_subplugins() {
430        global $CFG;
431
432        $types = core_component::get_plugin_types_with_subplugins();
433
434        // Hardcode it here to detect if anybody hacks the code to include more subplugin types.
435        $expected = array(
436            'mod' => "$CFG->dirroot/mod",
437            'editor' => "$CFG->dirroot/lib/editor",
438            'tool' => "$CFG->dirroot/$CFG->admin/tool",
439            'local' => "$CFG->dirroot/local",
440        );
441
442        $this->assertSame($expected, $types);
443
444    }
445
446    public function test_get_plugin_list_with_file() {
447        $this->resetAfterTest(true);
448
449        // No extra reset here because core_component reset automatically.
450
451        $expected = array();
452        $reports = core_component::get_plugin_list('report');
453        foreach ($reports as $name => $fulldir) {
454            if (file_exists("$fulldir/lib.php")) {
455                $expected[] = $name;
456            }
457        }
458
459        // Test cold.
460        $list = core_component::get_plugin_list_with_file('report', 'lib.php', false);
461        $this->assertEquals($expected, array_keys($list));
462
463        // Test hot.
464        $list = core_component::get_plugin_list_with_file('report', 'lib.php', false);
465        $this->assertEquals($expected, array_keys($list));
466
467        // Test with include.
468        $list = core_component::get_plugin_list_with_file('report', 'lib.php', true);
469        $this->assertEquals($expected, array_keys($list));
470
471        // Test missing.
472        $list = core_component::get_plugin_list_with_file('report', 'idontexist.php', true);
473        $this->assertEquals(array(), array_keys($list));
474    }
475
476    public function test_get_component_classes_in_namespace() {
477
478        // Unexisting.
479        $this->assertCount(0, core_component::get_component_classes_in_namespace('core_unexistingcomponent', 'something'));
480        $this->assertCount(0, core_component::get_component_classes_in_namespace('auth_cas', 'something'));
481
482        // Matches the last namespace level name not partials.
483        $this->assertCount(0, core_component::get_component_classes_in_namespace('auth_cas', 'tas'));
484        $this->assertCount(0, core_component::get_component_classes_in_namespace('core_user', 'course'));
485        $this->assertCount(0, core_component::get_component_classes_in_namespace('mod_forum', 'output\\emaildigest'));
486        $this->assertCount(0, core_component::get_component_classes_in_namespace('mod_forum', '\\output\\emaildigest'));
487        $this->assertCount(2, core_component::get_component_classes_in_namespace('mod_forum', 'output\\email'));
488        $this->assertCount(2, core_component::get_component_classes_in_namespace('mod_forum', '\\output\\email'));
489        $this->assertCount(2, core_component::get_component_classes_in_namespace('mod_forum', 'output\\email\\'));
490        $this->assertCount(2, core_component::get_component_classes_in_namespace('mod_forum', '\\output\\email\\'));
491
492        // Prefix with backslash if it doesn\'t come prefixed.
493        $this->assertCount(1, core_component::get_component_classes_in_namespace('auth_cas', 'task'));
494        $this->assertCount(1, core_component::get_component_classes_in_namespace('auth_cas', '\\task'));
495
496        // Core as a component works, the function can normalise the component name.
497        $this->assertCount(7, core_component::get_component_classes_in_namespace('core', 'update'));
498        $this->assertCount(7, core_component::get_component_classes_in_namespace('', 'update'));
499        $this->assertCount(7, core_component::get_component_classes_in_namespace('moodle', 'update'));
500
501        // Multiple levels.
502        $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', '\\output\\myprofile\\'));
503        $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', 'output\\myprofile\\'));
504        $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', '\\output\\myprofile'));
505        $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', 'output\\myprofile'));
506
507        // Without namespace it returns classes/ classes.
508        $this->assertCount(5, core_component::get_component_classes_in_namespace('tool_mobile', ''));
509        $this->assertCount(2, core_component::get_component_classes_in_namespace('tool_filetypes'));
510
511        // When no component is specified, classes are returned for the namespace in all components.
512        // (We don't assert exact amounts here as the count of `output` classes will change depending on plugins installed).
513        $this->assertGreaterThan(
514            count(\core_component::get_component_classes_in_namespace('core', 'output')),
515            count(\core_component::get_component_classes_in_namespace(null, 'output')));
516
517        // Without either a component or namespace it returns an empty array.
518        $this->assertEmpty(\core_component::get_component_classes_in_namespace());
519        $this->assertEmpty(\core_component::get_component_classes_in_namespace(null));
520        $this->assertEmpty(\core_component::get_component_classes_in_namespace(null, ''));
521    }
522
523    /**
524     * Data provider for classloader test
525     */
526    public function classloader_provider() {
527        global $CFG;
528
529        // As part of these tests, we Check that there are no unexpected problems with overlapping PSR namespaces.
530        // This is not in the spec, but may come up in some libraries using both namespaces and PEAR-style class names.
531        // If problems arise we can remove this test, but will need to add a warning.
532        // Normalise to forward slash for testing purposes.
533        $directory = str_replace('\\', '/', $CFG->dirroot) . "/lib/tests/fixtures/component/";
534
535        $psr0 = [
536          'psr0'      => 'lib/tests/fixtures/component/psr0',
537          'overlap'   => 'lib/tests/fixtures/component/overlap'
538        ];
539        $psr4 = [
540          'psr4'      => 'lib/tests/fixtures/component/psr4',
541          'overlap'   => 'lib/tests/fixtures/component/overlap'
542        ];
543        return [
544          'PSR-0 Classloading - Root' => [
545              'psr0' => $psr0,
546              'psr4' => $psr4,
547              'classname' => 'psr0_main',
548              'includedfiles' => "{$directory}psr0/main.php",
549          ],
550          'PSR-0 Classloading - Sub namespace - underscores' => [
551              'psr0' => $psr0,
552              'psr4' => $psr4,
553              'classname' => 'psr0_subnamespace_example',
554              'includedfiles' => "{$directory}psr0/subnamespace/example.php",
555          ],
556          'PSR-0 Classloading - Sub namespace - slashes' => [
557              'psr0' => $psr0,
558              'psr4' => $psr4,
559              'classname' => 'psr0\\subnamespace\\slashes',
560              'includedfiles' => "{$directory}psr0/subnamespace/slashes.php",
561          ],
562          'PSR-4 Classloading - Root' => [
563              'psr0' => $psr0,
564              'psr4' => $psr4,
565              'classname' => 'psr4\\main',
566              'includedfiles' => "{$directory}psr4/main.php",
567          ],
568          'PSR-4 Classloading - Sub namespace' => [
569              'psr0' => $psr0,
570              'psr4' => $psr4,
571              'classname' => 'psr4\\subnamespace\\example',
572              'includedfiles' => "{$directory}psr4/subnamespace/example.php",
573          ],
574          'PSR-4 Classloading - Ensure underscores are not converted to paths' => [
575              'psr0' => $psr0,
576              'psr4' => $psr4,
577              'classname' => 'psr4\\subnamespace\\underscore_example',
578              'includedfiles' => "{$directory}psr4/subnamespace/underscore_example.php",
579          ],
580          'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [
581              'psr0' => $psr0,
582              'psr4' => $psr4,
583              'classname' => 'overlap\\subnamespace\\example',
584              'includedfiles' => "{$directory}overlap/subnamespace/example.php",
585          ],
586          'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [
587              'psr0' => $psr0,
588              'psr4' => $psr4,
589              'classname' => 'overlap_subnamespace_example2',
590              'includedfiles' => "{$directory}overlap/subnamespace/example2.php",
591          ],
592        ];
593    }
594
595    /**
596     * Test the classloader.
597     *
598     * @dataProvider classloader_provider
599     * @param array $psr0 The PSR-0 namespaces to be used in the test.
600     * @param array $psr4 The PSR-4 namespaces to be used in the test.
601     * @param string $classname The name of the class to attempt to load.
602     * @param string $includedfiles The file expected to be loaded.
603     */
604    public function test_classloader($psr0, $psr4, $classname, $includedfiles) {
605        $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
606        $psr0namespaces->setAccessible(true);
607        $psr0namespaces->setValue(null, $psr0);
608
609        $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces');
610        $psr4namespaces->setAccessible(true);
611        $psr4namespaces->setValue(null, $psr4);
612
613        core_component::classloader($classname);
614        if (DIRECTORY_SEPARATOR != '/') {
615            // Denormalise the expected path so that we can quickly compare with get_included_files.
616            $includedfiles = str_replace('/', DIRECTORY_SEPARATOR, $includedfiles);
617        }
618        $this->assertContains($includedfiles, get_included_files());
619        $this->assertTrue(class_exists($classname, false));
620    }
621
622    /**
623     * Data provider for psr_classloader test
624     */
625    public function psr_classloader_provider() {
626        global $CFG;
627
628        // As part of these tests, we Check that there are no unexpected problems with overlapping PSR namespaces.
629        // This is not in the spec, but may come up in some libraries using both namespaces and PEAR-style class names.
630        // If problems arise we can remove this test, but will need to add a warning.
631        // Normalise to forward slash for testing purposes.
632        $directory = str_replace('\\', '/', $CFG->dirroot) . "/lib/tests/fixtures/component/";
633
634        $psr0 = [
635          'psr0'      => 'lib/tests/fixtures/component/psr0',
636          'overlap'   => 'lib/tests/fixtures/component/overlap'
637        ];
638        $psr4 = [
639          'psr4'      => 'lib/tests/fixtures/component/psr4',
640          'overlap'   => 'lib/tests/fixtures/component/overlap'
641        ];
642        return [
643          'PSR-0 Classloading - Root' => [
644              'psr0' => $psr0,
645              'psr4' => $psr4,
646              'classname' => 'psr0_main',
647              'file' => "{$directory}psr0/main.php",
648          ],
649          'PSR-0 Classloading - Sub namespace - underscores' => [
650              'psr0' => $psr0,
651              'psr4' => $psr4,
652              'classname' => 'psr0_subnamespace_example',
653              'file' => "{$directory}psr0/subnamespace/example.php",
654          ],
655          'PSR-0 Classloading - Sub namespace - slashes' => [
656              'psr0' => $psr0,
657              'psr4' => $psr4,
658              'classname' => 'psr0\\subnamespace\\slashes',
659              'file' => "{$directory}psr0/subnamespace/slashes.php",
660          ],
661          'PSR-0 Classloading - non-existant file' => [
662              'psr0' => $psr0,
663              'psr4' => $psr4,
664              'classname' => 'psr0_subnamespace_nonexistant_file',
665              'file' => false,
666          ],
667          'PSR-4 Classloading - Root' => [
668              'psr0' => $psr0,
669              'psr4' => $psr4,
670              'classname' => 'psr4\\main',
671              'file' => "{$directory}psr4/main.php",
672          ],
673          'PSR-4 Classloading - Sub namespace' => [
674              'psr0' => $psr0,
675              'psr4' => $psr4,
676              'classname' => 'psr4\\subnamespace\\example',
677              'file' => "{$directory}psr4/subnamespace/example.php",
678          ],
679          'PSR-4 Classloading - Ensure underscores are not converted to paths' => [
680              'psr0' => $psr0,
681              'psr4' => $psr4,
682              'classname' => 'psr4\\subnamespace\\underscore_example',
683              'file' => "{$directory}psr4/subnamespace/underscore_example.php",
684          ],
685          'PSR-4 Classloading - non-existant file' => [
686              'psr0' => $psr0,
687              'psr4' => $psr4,
688              'classname' => 'psr4\\subnamespace\\nonexistant',
689              'file' => false,
690          ],
691          'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [
692              'psr0' => $psr0,
693              'psr4' => $psr4,
694              'classname' => 'overlap\\subnamespace\\example',
695              'file' => "{$directory}overlap/subnamespace/example.php",
696          ],
697          'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [
698              'psr0' => $psr0,
699              'psr4' => $psr4,
700              'classname' => 'overlap_subnamespace_example2',
701              'file' => "{$directory}overlap/subnamespace/example2.php",
702          ],
703        ];
704    }
705
706    /**
707     * Test the PSR classloader.
708     *
709     * @dataProvider psr_classloader_provider
710     * @param array $psr0 The PSR-0 namespaces to be used in the test.
711     * @param array $psr4 The PSR-4 namespaces to be used in the test.
712     * @param string $classname The name of the class to attempt to load.
713     * @param string|bool $file The expected file corresponding to the class or false for nonexistant.
714     */
715    public function test_psr_classloader($psr0, $psr4, $classname, $file) {
716        $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
717        $psr0namespaces->setAccessible(true);
718        $psr0namespaces->setValue(null, $psr0);
719
720        $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces');
721        $psr4namespaces->setAccessible(true);
722        $oldpsr4namespaces = $psr4namespaces->getValue(null);
723        $psr4namespaces->setValue(null, $psr4);
724
725        $component = new ReflectionClass('core_component');
726        $psrclassloader = $component->getMethod('psr_classloader');
727        $psrclassloader->setAccessible(true);
728
729        $returnvalue = $psrclassloader->invokeArgs(null, array($classname));
730        // Normalise to forward slashes for testing comparison.
731        if ($returnvalue) {
732            $returnvalue = str_replace('\\', '/', $returnvalue);
733        }
734        $this->assertEquals($file, $returnvalue);
735    }
736
737    /**
738     * Data provider for get_class_file test
739     */
740    public function get_class_file_provider() {
741        global $CFG;
742
743        return [
744          'Getting a file with underscores' => [
745              'classname' => 'Test_With_Underscores',
746              'prefix' => "Test",
747              'path' => 'test/src',
748              'separators' => ['_'],
749              'result' => $CFG->dirroot . "/test/src/With/Underscores.php",
750          ],
751          'Getting a file with slashes' => [
752              'classname' => 'Test\\With\\Slashes',
753              'prefix' => "Test",
754              'path' => 'test/src',
755              'separators' => ['\\'],
756              'result' => $CFG->dirroot . "/test/src/With/Slashes.php",
757          ],
758          'Getting a file with multiple namespaces' => [
759              'classname' => 'Test\\With\\Multiple\\Namespaces',
760              'prefix' => "Test\\With",
761              'path' => 'test/src',
762              'separators' => ['\\'],
763              'result' => $CFG->dirroot . "/test/src/Multiple/Namespaces.php",
764          ],
765          'Getting a file with multiple namespaces' => [
766              'classname' => 'Nonexistant\\Namespace\\Test',
767              'prefix' => "Test",
768              'path' => 'test/src',
769              'separators' => ['\\'],
770              'result' => false,
771          ],
772        ];
773    }
774
775    /**
776     * Test the PSR classloader.
777     *
778     * @dataProvider get_class_file_provider
779     * @param string $classname the name of the class.
780     * @param string $prefix The namespace prefix used to identify the base directory of the source files.
781     * @param string $path The relative path to the base directory of the source files.
782     * @param string[] $separators The characters that should be used for separating.
783     * @param string|bool $result The expected result to be returned from get_class_file.
784     */
785    public function test_get_class_file($classname, $prefix, $path, $separators, $result) {
786        $component = new ReflectionClass('core_component');
787        $psrclassloader = $component->getMethod('get_class_file');
788        $psrclassloader->setAccessible(true);
789
790        $file = $psrclassloader->invokeArgs(null, array($classname, $prefix, $path, $separators));
791        $this->assertEquals($result, $file);
792    }
793
794    /**
795     * Confirm the get_component_list method contains an entry for every component.
796     */
797    public function test_get_component_list_contains_all_components() {
798        global $CFG;
799        $componentslist = \core_component::get_component_list();
800
801        // We should have an entry for each plugin type, and one additional for 'core'.
802        $plugintypes = \core_component::get_plugin_types();
803        $numelementsexpected = count($plugintypes) + 1;
804        $this->assertEquals($numelementsexpected, count($componentslist));
805
806        // And an entry for each of the plugin types.
807        foreach (array_keys($plugintypes) as $plugintype) {
808            $this->assertArrayHasKey($plugintype, $componentslist);
809        }
810
811        // And finally, one for 'core'.
812        $this->assertArrayHasKey('core', $componentslist);
813
814        // Check a few of the known plugin types to confirm their presence at their respective type index.
815        $this->assertEquals($componentslist['core']['core_comment'], $CFG->dirroot . '/comment');
816        $this->assertEquals($componentslist['mod']['mod_forum'], $CFG->dirroot . '/mod/forum');
817        $this->assertEquals($componentslist['tool']['tool_usertours'], $CFG->dirroot . '/' . $CFG->admin . '/tool/usertours');
818    }
819
820    /**
821     * Test the get_component_names() method.
822     */
823    public function test_get_component_names() {
824        global $CFG;
825        $componentnames = \core_component::get_component_names();
826
827        // We should have an entry for each plugin type.
828        $plugintypes = \core_component::get_plugin_types();
829        $numplugintypes = 0;
830        foreach ($plugintypes as $type => $typedir) {
831            foreach (\core_component::get_plugin_list($type) as $plugin) {
832                $numplugintypes++;
833            }
834        }
835        // And an entry for each core subsystem.
836        $numcomponents = $numplugintypes + count(\core_component::get_core_subsystems());
837
838        $this->assertEquals($numcomponents, count($componentnames));
839
840        // Check a few of the known plugin types to confirm their presence at their respective type index.
841        $this->assertContains('core_comment', $componentnames);
842        $this->assertContains('mod_forum', $componentnames);
843        $this->assertContains('tool_usertours', $componentnames);
844        $this->assertContains('core_favourites', $componentnames);
845    }
846}
847