Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  
  29  /**
  30   * Class core_component_testcase.
  31   */
  32  class 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 = 72;
  40  
  41      public function setUp(): void {
  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(): void {
  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->assertFileDoesNotExist("$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 (non-existent)' => [
 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  }