Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

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