. namespace core; use core\exception\coding_exception; use core\tests\fake_plugins_test_trait; use DirectoryIterator; use ReflectionClass; use ReflectionProperty; /** * component related tests. * * @package core * @category test * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * * @covers \core\component */ final class component_test extends \advanced_testcase { use fake_plugins_test_trait; #[\Override] public function tearDown(): void { parent::tearDown(); ini_set('error_log', null); } /** * To be changed if number of subsystems increases/decreases, * this is defined here to annoy devs that try to add more without any thinking, * always verify that it does not collide with any existing add-on modules and subplugins!!! */ const SUBSYSTEMCOUNT = 80; public function test_get_core_subsystems(): void { global $CFG; $subsystems = component::get_core_subsystems(); $this->assertCount( self::SUBSYSTEMCOUNT, $subsystems, 'Oh, somebody added or removed a core subsystem, think twice before doing that!', ); // Make sure all paths are full/null, exist and are inside dirroot. foreach ($subsystems as $subsystem => $fulldir) { $this->assertFalse(strpos($subsystem, '_'), 'Core subsystems must be one work without underscores'); if ($fulldir === null) { if ($subsystem === 'filepicker' || $subsystem === 'help') { // phpcs:ignore // Arrgghh, let's not introduce more subsystems for no real reason... } else { // Lang strings. $this->assertFileExists( "$CFG->dirroot/lang/en/$subsystem.php", 'Core subsystems without fulldir are usually used for lang strings.', ); } continue; } $this->assertFileExists($fulldir); // Check that base uses realpath() separators and "/" in the subdirs. $this->assertStringStartsWith($CFG->dirroot . '/', $fulldir); $reldir = substr($fulldir, strlen($CFG->dirroot) + 1); $this->assertFalse(strpos($reldir, '\\')); } // Make sure all core language files are also subsystems! $items = new DirectoryIterator("$CFG->dirroot/lang/en"); foreach ($items as $item) { if ($item->isDot() || $item->isDir()) { continue; } $file = $item->getFilename(); if ($file === 'moodle.php') { // Do not add new lang strings unless really necessary!!! continue; } if (substr($file, -4) !== '.php') { continue; } $file = substr($file, 0, strlen($file) - 4); $this->assertArrayHasKey( $file, $subsystems, 'All core lang files should be subsystems, think twice before adding anything!', ); } unset($item); unset($items); } public function test_deprecated_get_core_subsystems(): void { global $CFG; $subsystems = component::get_core_subsystems(); $this->assertSame($subsystems, get_core_subsystems(true)); $this->assertDebuggingCalled(); $this->resetDebugging(); $realsubsystems = get_core_subsystems(); $this->assertdebuggingcalledcount(2); $this->resetDebugging(); $this->assertSame($realsubsystems, get_core_subsystems(false)); $this->assertdebuggingcalledcount(2); $this->resetDebugging(); $this->assertEquals(count($subsystems), count($realsubsystems)); foreach ($subsystems as $subsystem => $fulldir) { $this->assertArrayHasKey($subsystem, $realsubsystems); if ($fulldir === null) { $this->assertNull($realsubsystems[$subsystem]); continue; } $this->assertSame($fulldir, $CFG->dirroot . '/' . $realsubsystems[$subsystem]); } } public function test_get_plugin_types(): void { global $CFG; $this->assertTrue( empty($CFG->themedir), 'Non-empty $CFG->themedir is not covered by any tests yet, you need to disable it.', ); $plugintypes = component::get_plugin_types(); foreach ($plugintypes as $plugintype => $fulldir) { $this->assertStringStartsWith("$CFG->dirroot/", $fulldir); } } public function test_deprecated_get_plugin_types(): void { global $CFG; $plugintypes = component::get_plugin_types(); $this->assertSame($plugintypes, get_plugin_types()); $this->assertDebuggingCalled(); $this->resetDebugging(); $this->assertSame($plugintypes, get_plugin_types(true)); $this->assertDebuggingCalled(); $this->resetDebugging(); $realplugintypes = get_plugin_types(false); $this->assertdebuggingcalledcount(2); $this->resetDebugging(); foreach ($plugintypes as $plugintype => $fulldir) { $this->assertSame($fulldir, $CFG->dirroot . '/' . $realplugintypes[$plugintype]); } } public function test_get_plugin_list(): void { global $CFG; $plugintypes = component::get_plugin_types(); foreach ($plugintypes as $plugintype => $fulldir) { $plugins = component::get_plugin_list($plugintype); foreach ($plugins as $pluginname => $plugindir) { $this->assertStringStartsWith("$CFG->dirroot/", $plugindir); } if ($plugintype !== 'auth') { // Let's crosscheck it with independent implementation (auth/db is an exception). $reldir = substr($fulldir, strlen($CFG->dirroot) + 1); $dirs = get_list_of_plugins($reldir); $dirs = array_values($dirs); $this->assertDebuggingCalled(); $this->assertSame($dirs, array_keys($plugins)); } } } public function test_deprecated_get_plugin_list(): void { $plugintypes = component::get_plugin_types(); foreach ($plugintypes as $plugintype => $fulldir) { $plugins = component::get_plugin_list($plugintype); $this->assertSame($plugins, get_plugin_list($plugintype)); $this->assertDebuggingCalled(); $this->resetDebugging(); } } public function test_get_plugin_directory(): void { $plugintypes = component::get_plugin_types(); foreach ($plugintypes as $plugintype => $fulldir) { $plugins = component::get_plugin_list($plugintype); foreach ($plugins as $pluginname => $plugindir) { $this->assertSame($plugindir, component::get_plugin_directory($plugintype, $pluginname)); } } } public function test_deprecated_get_plugin_directory(): void { $plugintypes = component::get_plugin_types(); foreach ($plugintypes as $plugintype => $fulldir) { $plugins = component::get_plugin_list($plugintype); foreach ($plugins as $pluginname => $plugindir) { $this->assertSame( component::get_plugin_directory($plugintype, $pluginname), get_plugin_directory($plugintype, $pluginname), ); $this->assertDebuggingCalled(); $this->resetDebugging(); } } } public function test_get_subsystem_directory(): void { $subsystems = component::get_core_subsystems(); foreach ($subsystems as $subsystem => $fulldir) { $this->assertSame($fulldir, component::get_subsystem_directory($subsystem)); } } /** * Test that the get_plugin_list_with_file() function returns the correct list of plugins. * * @covers \core\component::is_valid_plugin_name * @dataProvider is_valid_plugin_name_provider * @param array $arguments * @param bool $expected */ public function test_is_valid_plugin_name(array $arguments, bool $expected): void { $this->assertEquals($expected, component::is_valid_plugin_name(...$arguments)); } /** * Data provider for the is_valid_plugin_name function. * * @return array */ public static function is_valid_plugin_name_provider(): array { return [ [['mod', 'example1'], true], [['mod', 'feedback360'], true], [['mod', 'feedback_360'], false], [['mod', '2feedback'], false], [['mod', '1example'], false], [['mod', 'example.xx'], false], [['mod', '.example'], false], [['mod', '_example'], false], [['mod', 'example_'], false], [['mod', 'example_x1'], false], [['mod', 'example-x1'], false], [['mod', 'role'], false], [['tool', 'example1'], true], [['tool', 'example_x1'], true], [['tool', 'example_x1_xxx'], true], [['tool', 'feedback360'], true], [['tool', 'feed_back360'], true], [['tool', 'role'], true], [['tool', '1example'], false], [['tool', 'example.xx'], false], [['tool', 'example-xx'], false], [['tool', '.example'], false], [['tool', '_example'], false], [['tool', 'example_'], false], [['tool', 'example__x1'], false], // Some invalid cases. [['mod', null], false], [['mod', ''], false], [['tool', null], false], [['tool', ''], false], ]; } public function test_normalize_componentname(): void { // Moodle core. $this->assertSame('core', component::normalize_componentname('core')); $this->assertSame('core', component::normalize_componentname('moodle')); $this->assertSame('core', component::normalize_componentname('')); // Moodle core subsystems. $this->assertSame('core_admin', component::normalize_componentname('admin')); $this->assertSame('core_admin', component::normalize_componentname('core_admin')); $this->assertSame('core_admin', component::normalize_componentname('moodle_admin')); // Activity modules and their subplugins. $this->assertSame('mod_workshop', component::normalize_componentname('workshop')); $this->assertSame('mod_workshop', component::normalize_componentname('mod_workshop')); $this->assertSame('workshopform_accumulative', component::normalize_componentname('workshopform_accumulative')); $this->assertSame('mod_quiz', component::normalize_componentname('quiz')); $this->assertSame('quiz_grading', component::normalize_componentname('quiz_grading')); $this->assertSame('mod_data', component::normalize_componentname('data')); $this->assertSame('datafield_checkbox', component::normalize_componentname('datafield_checkbox')); // Other plugin types. $this->assertSame('auth_ldap', component::normalize_componentname('auth_ldap')); $this->assertSame('enrol_self', component::normalize_componentname('enrol_self')); $this->assertSame('block_html', component::normalize_componentname('block_html')); $this->assertSame('auth_oauth2', component::normalize_componentname('auth_oauth2')); $this->assertSame('local_amos', component::normalize_componentname('local_amos')); $this->assertSame('local_admin', component::normalize_componentname('local_admin')); // Unknown words without underscore are supposed to be activity modules. $this->assertSame( 'mod_whoonearthwouldcomewithsuchastupidnameofcomponent', component::normalize_componentname('whoonearthwouldcomewithsuchastupidnameofcomponent') ); // Module names can not contain underscores, this must be a subplugin. $this->assertSame( 'whoonearth_wouldcomewithsuchastupidnameofcomponent', component::normalize_componentname('whoonearth_wouldcomewithsuchastupidnameofcomponent') ); $this->assertSame( 'whoonearth_would_come_withsuchastupidnameofcomponent', component::normalize_componentname('whoonearth_would_come_withsuchastupidnameofcomponent') ); } /** * Test \core_component::normalize_component function. * * @dataProvider normalise_component_provider * @param array $expected * @param string $args */ public function test_normalize_component(array $expected, string $args): void { $this->assertSame( $expected, component::normalize_component($args), ); } /** * Test the deprecated normalize_component function. * * @dataProvider normalise_component_provider * @param array $expected * @param string $args */ public function test_deprecated_normalize_component(array $expected, string $args): void { $this->assertSame( $expected, normalize_component($args), ); $this->assertDebuggingCalled(); } /** * Data provider for the normalize_component function. */ public static function normalise_component_provider(): array { return [ // Moodle core. [['core', null], 'core'], [['core', null], ''], [['core', null], 'moodle'], // Moodle core subsystems. [['core', 'admin'], 'admin'], [['core', 'admin'], 'core_admin'], [['core', 'admin'], 'moodle_admin'], // Activity modules and their subplugins. [['mod', 'workshop'], 'workshop'], [['mod', 'workshop'], 'mod_workshop'], [['workshopform', 'accumulative'], 'workshopform_accumulative'], [['mod', 'quiz'], 'quiz'], [['quiz', 'grading'], 'quiz_grading'], [['mod', 'data'], 'data'], [['datafield', 'checkbox'], 'datafield_checkbox'], // Other plugin types. [['auth', 'ldap'], 'auth_ldap'], [['enrol', 'self'], 'enrol_self'], [['block', 'html'], 'block_html'], [['auth', 'oauth2'], 'auth_oauth2'], [['local', 'amos'], 'local_amos'], [['local', 'admin'], 'local_admin'], // Unknown words without underscore are supposed to be activity modules. [ ['mod', 'whoonearthwouldcomewithsuchastupidnameofcomponent'], 'whoonearthwouldcomewithsuchastupidnameofcomponent', ], // Module names can not contain underscores, this must be a subplugin. [ ['whoonearth', 'wouldcomewithsuchastupidnameofcomponent'], 'whoonearth_wouldcomewithsuchastupidnameofcomponent', ], [ ['whoonearth', 'would_come_withsuchastupidnameofcomponent'], 'whoonearth_would_come_withsuchastupidnameofcomponent', ], ]; } public function test_get_component_directory(): void { $plugintypes = component::get_plugin_types(); foreach ($plugintypes as $plugintype => $fulldir) { $plugins = component::get_plugin_list($plugintype); foreach ($plugins as $pluginname => $plugindir) { $this->assertSame($plugindir, component::get_component_directory(($plugintype . '_' . $pluginname))); } } $subsystems = component::get_core_subsystems(); foreach ($subsystems as $subsystem => $fulldir) { $this->assertSame($fulldir, component::get_component_directory(('core_' . $subsystem))); } } /** * Unit tests for get_component_from_classname. * * @dataProvider get_component_from_classname_provider * @param string $classname The class name to test * @param string|null $expected The expected component * @covers \core\component::get_component_from_classname */ public function test_get_component_from_classname( string $classname, string|null $expected, ): void { $this->assertEquals( $expected, component::get_component_from_classname($classname), ); } /** * Data provider for get_component_from_classname tests. * * @return array */ public static function get_component_from_classname_provider(): array { // Start off with testcases which have the leading \. $testcases = [ // Core. [\core\example::class, 'core'], // A core subsystem. [\core_message\example::class, 'core_message'], // A fake core subsystem. [\core_fake\example::class, null], // A plugin. [\mod_forum\example::class, 'mod_forum'], // A plugin in the old style is not supported. [\mod_forum_example::class, null], // A fake plugin. [\mod_fake\example::class, null], // A subplugin. [\tiny_link\example::class, 'tiny_link'], ]; // Duplicate the testcases, adding a nested namespace. $testcases = array_merge( $testcases, array_map( fn ($testcase) => [$testcase[0] . '\\in\\sub\\directory', $testcase[1]], $testcases, ), ); // Duplicate the testcases, removing the leading \. return array_merge( $testcases, array_map( fn ($testcase) => [ltrim($testcase[0], '\\'), $testcase[1]], $testcases, ), ); } public function test_deprecated_get_component_directory(): void { $plugintypes = component::get_plugin_types(); foreach ($plugintypes as $plugintype => $fulldir) { $plugins = component::get_plugin_list($plugintype); foreach ($plugins as $pluginname => $plugindir) { $this->assertSame($plugindir, get_component_directory(($plugintype . '_' . $pluginname))); $this->assertDebuggingCalled(); $this->resetDebugging(); } } $subsystems = component::get_core_subsystems(); foreach ($subsystems as $subsystem => $fulldir) { $this->assertSame($fulldir, get_component_directory(('core_' . $subsystem))); $this->assertDebuggingCalled(); $this->resetDebugging(); } } public function test_get_subtype_parent(): void { global $CFG; $this->assertNull(component::get_subtype_parent('mod')); // Any plugin with more subtypes is ok here. $this->assertFileExists("$CFG->dirroot/mod/assign/db/subplugins.json"); $this->assertSame('mod_assign', component::get_subtype_parent('assignsubmission')); $this->assertSame('mod_assign', component::get_subtype_parent('assignfeedback')); $this->assertNull(component::get_subtype_parent('assignxxxxx')); } public function test_get_subplugins(): void { global $CFG; // Any plugin with more subtypes is ok here. $this->assertFileExists("$CFG->dirroot/mod/assign/db/subplugins.json"); $subplugins = component::get_subplugins('mod_assign'); $this->assertSame(['assignsubmission', 'assignfeedback'], array_keys($subplugins)); $subs = component::get_plugin_list('assignsubmission'); $feeds = component::get_plugin_list('assignfeedback'); $this->assertSame(array_keys($subs), $subplugins['assignsubmission']); $this->assertSame(array_keys($feeds), $subplugins['assignfeedback']); // Any plugin without subtypes is ok here. $this->assertFileExists("$CFG->dirroot/mod/choice"); $this->assertFileDoesNotExist("$CFG->dirroot/mod/choice/db/subplugins.json"); $this->assertNull(component::get_subplugins('mod_choice')); $this->assertNull(component::get_subplugins('xxxx_yyyy')); } public function test_get_plugin_types_with_subplugins(): void { global $CFG; $types = component::get_plugin_types_with_subplugins(); // Hardcode it here to detect if anybody hacks the code to include more subplugin types. $expected = [ 'mod' => "$CFG->dirroot/mod", 'editor' => "$CFG->dirroot/lib/editor", 'tool' => "$CFG->dirroot/$CFG->admin/tool", 'local' => "$CFG->dirroot/local", ]; $this->assertSame($expected, $types); } public function test_get_plugin_list_with_file(): void { $this->resetAfterTest(true); // No extra reset here because component reset automatically. $expected = []; $reports = component::get_plugin_list('report'); foreach ($reports as $name => $fulldir) { if (file_exists("$fulldir/lib.php")) { $expected[] = $name; } } // Test cold. $list = component::get_plugin_list_with_file('report', 'lib.php', false); $this->assertEquals($expected, array_keys($list)); // Test hot. $list = component::get_plugin_list_with_file('report', 'lib.php', false); $this->assertEquals($expected, array_keys($list)); // Test with include. $list = component::get_plugin_list_with_file('report', 'lib.php', true); $this->assertEquals($expected, array_keys($list)); // Test missing. $list = component::get_plugin_list_with_file('report', 'idontexist.php', true); $this->assertEquals([], array_keys($list)); } /** * Tests for get_component_classes_in_namespace. */ public function test_get_component_classes_in_namespace(): void { // Unexisting. $this->assertCount(0, component::get_component_classes_in_namespace('core_unexistingcomponent', 'something')); $this->assertCount(0, component::get_component_classes_in_namespace('auth_db', 'something')); // Matches the last namespace level name not partials. $this->assertCount(0, component::get_component_classes_in_namespace('auth_db', 'tas')); $this->assertCount(0, component::get_component_classes_in_namespace('core_user', 'course')); $this->assertCount(0, component::get_component_classes_in_namespace('mod_forum', 'output\\emaildigest')); $this->assertCount(0, component::get_component_classes_in_namespace('mod_forum', '\\output\\emaildigest')); // Without either a component or namespace it returns an empty array. $this->assertEmpty(component::get_component_classes_in_namespace()); $this->assertEmpty(component::get_component_classes_in_namespace(null)); $this->assertEmpty(component::get_component_classes_in_namespace(null, '')); } /** * Test that the get_component_classes_in_namespace() function returns classes in the correct namespace. * * @dataProvider get_component_classes_in_namespace_provider * @param array $methodargs * @param string $expectedclassnameformat */ public function test_get_component_classes_in_namespace_provider( array $methodargs, string $expectedclassnameformat, ): void { $classlist = component::get_component_classes_in_namespace(...$methodargs); $this->assertGreaterThan(0, count($classlist)); foreach (array_keys($classlist) as $classname) { $this->assertStringMatchesFormat($expectedclassnameformat, $classname); } } /** * Data provider for get_component_classes_in_namespace tests. * * @return array */ public static function get_component_classes_in_namespace_provider(): array { return [ // Matches the last namespace level name not partials. [ ['mod_forum', 'output\\email'], 'mod_forum\output\email\%s', ], [ ['mod_forum', '\\output\\email'], 'mod_forum\output\email\%s', ], [ ['mod_forum', 'output\\email\\'], 'mod_forum\output\email\%s', ], [ ['mod_forum', '\\output\\email\\'], 'mod_forum\output\email\%s', ], // Prefix with backslash if it doesn\'t come prefixed. [ ['auth_db', 'task'], 'auth_db\task\%s', ], [ ['auth_db', '\\task'], 'auth_db\task\%s', ], // Core as a component works, the function can normalise the component name. [ ['core', 'update'], 'core\update\%s', ], [ ['', 'update'], 'core\update\%s', ], [ ['moodle', 'update'], 'core\update\%s', ], // Multiple levels. [ ['core_user', '\\output\\myprofile\\'], 'core_user\output\myprofile\%s', ], [ ['core_user', 'output\\myprofile\\'], 'core_user\output\myprofile\%s', ], [ ['core_user', '\\output\\myprofile'], 'core_user\output\myprofile\%s', ], [ ['core_user', 'output\\myprofile'], 'core_user\output\myprofile\%s', ], // Without namespace it returns classes/ classes. [ ['tool_mobile', ''], 'tool_mobile\%s', ], [ ['tool_filetypes'], 'tool_filetypes\%s', ], // Multiple levels. [ ['core_user', '\\output\\myprofile\\'], 'core_user\output\myprofile\%s', ], // When no component is specified, classes are returned for the namespace in all components. // (We don't assert exact amounts here as the count of `output` classes will change depending on plugins installed). [ ['core', 'output'], 'core\%s', ], [ [null, 'output'], '%s', ], ]; } /** * Data provider for classloader test */ public static function classloader_provider(): array { global $CFG; // As part of these tests, we Check that there are no unexpected problems with overlapping PSR namespaces. // This is not in the spec, but may come up in some libraries using both namespaces and PEAR-style class names. // If problems arise we can remove this test, but will need to add a warning. // Normalise to forward slash for testing purposes. $directory = str_replace('\\', '/', $CFG->dirroot) . "/lib/tests/fixtures/component/"; $psr0 = [ 'psr0' => 'lib/tests/fixtures/component/psr0', 'overlap' => 'lib/tests/fixtures/component/overlap', ]; $psr4 = [ 'psr4' => 'lib/tests/fixtures/component/psr4', 'overlap' => 'lib/tests/fixtures/component/overlap', ]; return [ 'PSR-0 Classloading - Root' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'psr0_main', 'includedfiles' => "{$directory}psr0/main.php", ], 'PSR-0 Classloading - Sub namespace - underscores' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'psr0_subnamespace_example', 'includedfiles' => "{$directory}psr0/subnamespace/example.php", ], 'PSR-0 Classloading - Sub namespace - slashes' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'psr0\\subnamespace\\slashes', 'includedfiles' => "{$directory}psr0/subnamespace/slashes.php", ], 'PSR-4 Classloading - Root' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'psr4\\main', 'includedfiles' => "{$directory}psr4/main.php", ], 'PSR-4 Classloading - Sub namespace' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'psr4\\subnamespace\\example', 'includedfiles' => "{$directory}psr4/subnamespace/example.php", ], 'PSR-4 Classloading - Ensure underscores are not converted to paths' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'psr4\\subnamespace\\underscore_example', 'includedfiles' => "{$directory}psr4/subnamespace/underscore_example.php", ], 'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'overlap\\subnamespace\\example', 'includedfiles' => "{$directory}overlap/subnamespace/example.php", ], 'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'overlap_subnamespace_example2', 'includedfiles' => "{$directory}overlap/subnamespace/example2.php", ], ]; } /** * Test the classloader. * * @dataProvider classloader_provider * @param array $psr0 The PSR-0 namespaces to be used in the test. * @param array $psr4 The PSR-4 namespaces to be used in the test. * @param string $classname The name of the class to attempt to load. * @param string $includedfiles The file expected to be loaded. * @runInSeparateProcess */ public function test_classloader($psr0, $psr4, $classname, $includedfiles): void { $psr0namespaces = new ReflectionProperty(component::class, 'psr0namespaces'); $psr0namespaces->setValue(null, $psr0); $psr4namespaces = new ReflectionProperty(component::class, 'psr4namespaces'); $psr4namespaces->setValue(null, $psr4); component::classloader($classname); if (DIRECTORY_SEPARATOR != '/') { // Denormalise the expected path so that we can quickly compare with get_included_files. $includedfiles = str_replace('/', DIRECTORY_SEPARATOR, $includedfiles); } $this->assertContains($includedfiles, get_included_files()); $this->assertTrue(class_exists($classname, false)); } /** * Data provider for psr_classloader test */ public static function psr_classloader_provider(): array { global $CFG; // As part of these tests, we Check that there are no unexpected problems with overlapping PSR namespaces. // This is not in the spec, but may come up in some libraries using both namespaces and PEAR-style class names. // If problems arise we can remove this test, but will need to add a warning. // Normalise to forward slash for testing purposes. $dirroot = str_replace('\\', '/', $CFG->dirroot); $directory = "{$dirroot}/lib/tests/fixtures/component/"; $psr0 = [ 'psr0' => 'lib/tests/fixtures/component/psr0', 'overlap' => 'lib/tests/fixtures/component/overlap', ]; $psr4 = [ 'psr4' => 'lib/tests/fixtures/component/psr4', 'overlap' => 'lib/tests/fixtures/component/overlap', ]; return [ 'PSR-0 Classloading - Root' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'psr0_main', 'file' => "{$directory}psr0/main.php", ], 'PSR-0 Classloading - Sub namespace - underscores' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'psr0_subnamespace_example', 'file' => "{$directory}psr0/subnamespace/example.php", ], 'PSR-0 Classloading - Sub namespace - slashes' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'psr0\\subnamespace\\slashes', 'file' => "{$directory}psr0/subnamespace/slashes.php", ], 'PSR-0 Classloading - non-existent file' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'psr0_subnamespace_nonexistent_file', 'file' => false, ], 'PSR-4 Classloading - Root' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'psr4\\main', 'file' => "{$directory}psr4/main.php", ], 'PSR-4 Classloading - Sub namespace' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'psr4\\subnamespace\\example', 'file' => "{$directory}psr4/subnamespace/example.php", ], 'PSR-4 Classloading - Ensure underscores are not converted to paths' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'psr4\\subnamespace\\underscore_example', 'file' => "{$directory}psr4/subnamespace/underscore_example.php", ], 'PSR-4 Classloading - non-existent file' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'psr4\\subnamespace\\nonexistent', 'file' => false, ], 'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'overlap\\subnamespace\\example', 'file' => "{$directory}overlap/subnamespace/example.php", ], 'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [ 'psr0' => $psr0, 'psr4' => $psr4, 'classname' => 'overlap_subnamespace_example2', 'file' => "{$directory}overlap/subnamespace/example2.php", ], 'PSR-4 namespaces can come from multiple sources - first source' => [ 'psr0' => $psr0, 'psr4' => [ 'Psr\\Http\\Message' => [ 'lib/psr/http-message/src', 'lib/psr/http-factory/src', ], ], 'classname' => 'Psr\Http\Message\ServerRequestInterface', 'file' => "{$dirroot}/lib/psr/http-message/src/ServerRequestInterface.php", ], 'PSR-4 namespaces can come from multiple sources - second source' => [ 'psr0' => [], 'psr4' => [ 'Psr\\Http\\Message' => [ 'lib/psr/http-message/src', 'lib/psr/http-factory/src', ], ], 'classname' => 'Psr\Http\Message\ServerRequestFactoryInterface', 'file' => "{$dirroot}/lib/psr/http-factory/src/ServerRequestFactoryInterface.php", ], ]; } /** * Test that the classloader can load from the test namespaces. */ public function test_classloader_tests_namespace(): void { global $CFG; $this->resetAfterTest(); $getclassfilecontent = function (string $classname, ?string $namespace): string { if ($namespace) { $content = " [ 'classes' => [ 'example.php' => $getclassfilecontent('example', 'core'), ], 'tests' => [ 'classes' => [ 'example_classname.php' => $getclassfilecontent('example_classname', \core\tests::class), ], 'behat' => [ 'example_classname.php' => $getclassfilecontent('example_classname', \core\behat::class), ], ], ], ]); // Note: This is pretty hacky, but it's the only way to test the classloader. // We have to override the dirroot and libdir, and then reset the plugintypes property. $CFG->dirroot = $vfileroot->url(); $CFG->libdir = $vfileroot->url() . '/lib'; component::reset(); // Existing classes do not break. $this->assertTrue( class_exists(\core\example::class), ); // Test and behat classes work. $this->assertTrue( class_exists(\core\tests\example_classname::class), ); $this->assertTrue( class_exists(\core\behat\example_classname::class), ); // Non-existent classes do not do anything. $this->assertFalse( class_exists(\core\tests\example_classname_not_found::class), ); } /** * Test the PSR classloader. * * @dataProvider psr_classloader_provider * @param array $psr0 The PSR-0 namespaces to be used in the test. * @param array $psr4 The PSR-4 namespaces to be used in the test. * @param string $classname The name of the class to attempt to load. * @param string|bool $file The expected file corresponding to the class or false for nonexistant. * @runInSeparateProcess */ public function test_psr_classloader($psr0, $psr4, $classname, $file): void { return; $psr0namespaces = new ReflectionProperty(component::class, 'psr0namespaces'); $psr0namespaces->setValue(null, $psr0); $psr4namespaces = new ReflectionProperty(component::class, 'psr4namespaces'); $psr4namespaces->setValue(null, $psr4); $component = new ReflectionClass(component::class); $psrclassloader = $component->getMethod('psr_classloader'); $returnvalue = $psrclassloader->invokeArgs(null, [$classname]); // Normalise to forward slashes for testing comparison. if ($returnvalue) { $returnvalue = str_replace('\\', '/', $returnvalue); } $this->assertEquals($file, $returnvalue); } /** * Data provider for get_class_file test */ public static function get_class_file_provider(): array { global $CFG; return [ 'Getting a file with underscores' => [ 'classname' => 'Test_With_Underscores', 'prefix' => "Test", 'path' => 'test/src', 'separators' => ['_'], 'result' => $CFG->dirroot . "/test/src/With/Underscores.php", ], 'Getting a file with slashes' => [ 'classname' => 'Test\\With\\Slashes', 'prefix' => "Test", 'path' => 'test/src', 'separators' => ['\\'], 'result' => $CFG->dirroot . "/test/src/With/Slashes.php", ], 'Getting a file with multiple namespaces' => [ 'classname' => 'Test\\With\\Multiple\\Namespaces', 'prefix' => "Test\\With", 'path' => 'test/src', 'separators' => ['\\'], 'result' => $CFG->dirroot . "/test/src/Multiple/Namespaces.php", ], 'Getting a file with multiple namespaces (non-existent)' => [ 'classname' => 'Nonexistent\\Namespace\\Test', 'prefix' => "Test", 'path' => 'test/src', 'separators' => ['\\'], 'result' => false, ], ]; } /** * Test the PSR classloader. * * @dataProvider get_class_file_provider * @param string $classname the name of the class. * @param string $prefix The namespace prefix used to identify the base directory of the source files. * @param string $path The relative path to the base directory of the source files. * @param string[] $separators The characters that should be used for separating. * @param string|bool $result The expected result to be returned from get_class_file. */ public function test_get_class_file($classname, $prefix, $path, $separators, $result): void { $component = new ReflectionClass(component::class); $psrclassloader = $component->getMethod('get_class_file'); $file = $psrclassloader->invokeArgs(null, [$classname, $prefix, $path, $separators]); $this->assertEquals($result, $file); } /** * Confirm the get_component_list method contains an entry for every component. */ public function test_get_component_list_contains_all_components(): void { global $CFG; $componentslist = component::get_component_list(); // We should have an entry for each plugin type, and one additional for 'core'. $plugintypes = component::get_plugin_types(); $numelementsexpected = count($plugintypes) + 1; $this->assertEquals($numelementsexpected, count($componentslist)); // And an entry for each of the plugin types. foreach (array_keys($plugintypes) as $plugintype) { $this->assertArrayHasKey($plugintype, $componentslist); } // And one for 'core'. $this->assertArrayHasKey('core', $componentslist); // Check a few of the known plugin types to confirm their presence at their respective type index. $this->assertEquals($componentslist['core']['core_comment'], $CFG->dirroot . '/comment'); $this->assertEquals($componentslist['mod']['mod_forum'], $CFG->dirroot . '/mod/forum'); $this->assertEquals($componentslist['tool']['tool_usertours'], $CFG->dirroot . '/' . $CFG->admin . '/tool/usertours'); } /** * Test the get_component_names() method. * * @dataProvider get_component_names_provider * @param bool $includecore Whether to include core in the list. * @param bool $coreexpected Whether core is expected to be in the list. */ public function test_get_component_names( bool $includecore, bool $coreexpected, ): void { global $CFG; $componentnames = component::get_component_names($includecore); // We should have an entry for each plugin type. $plugintypes = component::get_plugin_types(); $numplugintypes = 0; foreach (array_keys($plugintypes) as $type) { $numplugintypes += count(component::get_plugin_list($type)); } // And an entry for each core subsystem. $numcomponents = $numplugintypes + count(component::get_core_subsystems()); if ($coreexpected) { // Add one for core. $numcomponents++; } $this->assertEquals($numcomponents, count($componentnames)); // Check a few of the known plugin types to confirm their presence at their respective type index. $this->assertContains('core_comment', $componentnames); $this->assertContains('mod_forum', $componentnames); $this->assertContains('tool_usertours', $componentnames); $this->assertContains('core_favourites', $componentnames); if ($coreexpected) { $this->assertContains('core', $componentnames); } else { $this->assertNotContains('core', $componentnames); } } /** * Data provider for get_component_names() test. * * @return array */ public static function get_component_names_provider(): array { return [ [false, false], [true, true], ]; } /** * Basic tests for APIs related functions in the component class. */ public function test_apis_methods(): void { $apis = component::get_core_apis(); $this->assertIsArray($apis); $apinames = component::get_core_api_names(); $this->assertIsArray($apis); // Both should return the very same APIs. $this->assertEquals($apinames, array_keys($apis)); $this->assertFalse(component::is_core_api('lalala')); $this->assertTrue(component::is_core_api('privacy')); } /** * Test that the apis.json structure matches expectations * * While we include an apis.schema.json file in core, there isn't any PHP built-in allowing us * to validate it (3rd part libraries needed). Plus the schema doesn't allow to validate things * like uniqueness or sorting. We are going to do all that here. */ public function test_apis_json_validation(): void { $apis = $sortedapis = component::get_core_apis(); ksort($sortedapis); // We'll need this later. $subsystems = component::get_core_subsystems(); // To verify all apis are pointing to valid subsystems. $subsystems['core'] = 'anything'; // Let's add 'core' because it's a valid component for apis. // General structure validations. $this->assertIsArray($apis); $this->assertGreaterThan(25, count($apis)); $this->assertArrayHasKey('privacy', $apis); // Verify a few. $this->assertArrayHasKey('external', $apis); $this->assertArrayHasKey('search', $apis); $this->assertEquals(array_keys($sortedapis), array_keys($apis)); // Verify json is sorted alphabetically. // Iterate over all apis and perform more validations. foreach ($apis as $apiname => $attributes) { // Message, to be used later and easier finding the problem. $message = "Validation problem found with API: {$apiname}"; $this->assertIsObject($attributes, $message); $this->assertMatchesRegularExpression('/^[a-z][a-z0-9]+$/', $apiname, $message); $this->assertEquals(['component', 'allowedlevel2', 'allowedspread'], array_keys((array)$attributes), $message); // Verify attributes. if ($apiname !== 'core') { // Exception for core api, it doesn't have component. // Check that component attribute looks correct. $this->assertMatchesRegularExpression('/^(core|[a-z][a-z0-9_]+)$/', $attributes->component, $message); // Ensure that the api component (without the core_ prefix) is a correct subsystem. $this->assertArrayHasKey(str_replace('core_', '', $attributes->component), $subsystems, $message); } else { $this->assertNull($attributes->component, $message); } // Now check for the rest of attributes. $this->assertIsBool($attributes->allowedlevel2, $message); $this->assertIsBool($attributes->allowedspread, $message); // Cannot spread if level2 is not allowed. $this->assertLessThanOrEqual($attributes->allowedlevel2, $attributes->allowedspread, $message); } } /** * Test for monologo icons check in plugins. */ public function test_has_monologo_icon(): void { // The Forum activity plugin has monologo icons. $this->assertTrue(component::has_monologo_icon('mod', 'forum')); // The core H5P subsystem doesn't have monologo icons. $this->assertFalse(component::has_monologo_icon('core', 'h5p')); // The function will return false for a non-existent component. $this->assertFalse(component::has_monologo_icon('randomcomponent', 'h5p')); } /* * Tests the getter for the db directory summary hash. * * @covers \core\component::get_all_directory_hashes */ public function test_get_db_directories_hash(): void { $initial = component::get_all_component_hash(); $dir = make_request_directory(); $hashes = component::get_all_directory_hashes([$dir]); $emptydirhash = component::get_all_component_hash([$hashes]); // Confirm that a single empty directory is a different hash to the core hash. $this->assertNotEquals($initial, $emptydirhash); // Now lets add something to the dir, and check the hash is different. $file = fopen($dir . '/test.php', 'w'); fwrite($file, 'sometestdata'); fclose($file); $hashes = component::get_all_directory_hashes([$dir]); $onefiledirhash = component::get_all_component_hash([$hashes]); $this->assertNotEquals($emptydirhash, $onefiledirhash); // Now add a subdirectory inside the request dir. This should not affect the hash. mkdir($dir . '/subdir'); $hashes = component::get_all_directory_hashes([$dir]); $finalhash = component::get_all_component_hash([$hashes]); $this->assertEquals($onefiledirhash, $finalhash); } /** * Data provider fetching all third-party lib directories. * * @return array */ public static function core_thirdparty_libs_provider(): array { global $CFG; $libs = []; $xmlpath = $CFG->libdir . '/thirdpartylibs.xml'; $xml = simplexml_load_file($xmlpath); foreach ($xml as $lib) { $base = realpath(dirname($xmlpath)); $fullpath = "{$base}/{$lib->location}"; $relativepath = substr($fullpath, strlen($CFG->dirroot)); $libs[$relativepath] = [ 'name' => (string) $lib->name, 'fullpath' => $fullpath, 'relativepath' => $relativepath, ]; } return $libs; } /** * Data provider fetching all third-party lib directories with a composer.json file. * * @return array */ public static function core_thirdparty_libs_with_composer_provider(): array { return array_filter(self::core_thirdparty_libs_provider(), function ($lib) { return file_exists("{$lib['fullpath']}/composer.json"); }); } /** * Summary of test_composer_files * * @dataProvider core_thirdparty_libs_with_composer_provider * @param string $name * @param string $fullpath * @param string $relativepath */ public function test_composer_files( string $name, string $fullpath, string $relativepath, ): void { $this->assertFileExists("{$fullpath}/composer.json"); $composer = json_decode(file_get_contents("{$fullpath}/composer.json"), true); $rc = new ReflectionClass(\core\component::class); if (array_key_exists('autoload', $composer)) { // Check that the PSR-4 namespaces are present and correct. if (array_key_exists('psr-4', $composer['autoload'])) { $autoloadnamespaces = $rc->getProperty('psr4namespaces')->getValue(null); foreach ($composer['autoload']['psr-4'] as $namespace => $path) { // Composer PSR-4 namespace autoloads may optionally have a trailing slash. Standardise the value. $namespace = rtrim($namespace, '\\'); // If it exists in the composer.json the namespace must exist in our autoloader. $this->assertArrayHasKey($namespace, $autoloadnamespaces); // Ours should be standardised to not have a trailing slash. $this->assertEquals( rtrim($relativepath, '/'), $relativepath, "Moodle PSR-4 namespaces must have no trailing /", ); // The composer.json can specify an array of possible values. // Standardise the format to the array format. $paths = is_array($path) ? $path : [$path]; foreach ($paths as $path) { // The composer.json can specify any arbitrary directory within the folder. // It always contains a leading slash (/) or backslash (\) on Windows. // It may also have an optional trailing slash (/). // Concatenate the parts and removes the slashes. $relativenamespacepath = trim("{$relativepath}/{$path}", '/\\'); // The Moodle PSR-4 autoloader data has two formats: // - a string, for a single source; or // - an array, for multiple sources. // Standardise the format to the latter format. if (!is_array($autoloadnamespaces[$namespace])) { $autoloadnamespaces[$namespace] = [$autoloadnamespaces[$namespace]]; } // Ensure that the autoloader contains the normalised path. $this->assertContains( $relativenamespacepath, $autoloadnamespaces[$namespace], "Moodle PSR-4 namespace missing entry for library {$name}: {$namespace} => {$relativenamespacepath}", ); } } } // Check that the composer autoload files are present. if (array_key_exists('files', $composer['autoload'])) { // The Moodle composer file autoloads are a simple string[]. $autoloadnamefiles = $rc->getProperty('composerautoloadfiles')->getValue(null); foreach ($composer['autoload']['files'] as $file) { $this->assertContains(trim($relativepath, '/\\') . "/{$file}", $autoloadnamefiles); } } } } /** * Test that fetching of subtype data throws an exception when a subplugins.php is present without a json equivalent. */ public function test_fetch_subtypes_php_only(): void { $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, [ 'plugintype' => [ 'exampleplugin' => [ 'db' => [ 'subplugins.php' => '', ], ], ], ]); $this->expectException(coding_exception::class); $this->expectExceptionMessageMatches('/Use of subplugins.php has been deprecated and is no longer supported/'); $pluginroot = $vfileroot->getChild('plugintype/exampleplugin'); $rcm = new \ReflectionMethod(\core\component::class, 'fetch_subtypes'); $rcm->invoke(null, $pluginroot->url()); } /** * Test that fetching of subtype data does not throw an exception when a subplugins.php is present * with a json file equivalent. * * Note: The content of the php file is irrelevant and we no longer use it anyway. */ public function test_fetch_subtypes_php_and_json(): void { global $CFG; $this->resetAfterTest(); $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, [ 'plugintype' => [ 'exampleplugin' => [ 'db' => [ 'subplugins.json' => json_encode([ 'subplugintypes' => [ 'exampleplugina' => 'apples', ], ]), 'subplugins.php' => '', ], 'apples' => [], ], ], ]); $CFG->dirroot = $vfileroot->url(); $pluginroot = $vfileroot->getChild('plugintype/exampleplugin'); $rcm = new \ReflectionMethod(\core\component::class, 'fetch_subtypes'); $subplugins = $rcm->invoke(null, $pluginroot->url()); $this->assertEquals([ 'plugintypes' => [ 'exampleplugina' => $pluginroot->getChild('apples')->url(), ], ], $subplugins); } /** * Test that fetching of subtype data in a file which is missing the new subplugintypes key warns. */ public function test_fetch_subtypes_plugintypes_only(): void { global $CFG; $this->resetAfterTest(); $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, [ 'plugintype' => [ 'exampleplugin' => [ 'db' => [ 'subplugins.json' => json_encode([ 'plugintypes' => [ 'exampleplugina' => 'plugintype/exampleplugin/apples', ], ]), 'subplugins.php' => '', ], 'apples' => [], ], ], ]); $CFG->dirroot = $vfileroot->url(); $pluginroot = $vfileroot->getChild('plugintype/exampleplugin'); $logdir = make_request_directory(); $logfile = "{$logdir}/error.log"; ini_set('error_log', $logfile); $rcm = new \ReflectionMethod(\core\component::class, 'fetch_subtypes'); $subplugins = $rcm->invoke(null, $pluginroot->url()); $this->assertEquals([ 'plugintypes' => [ 'exampleplugina' => $pluginroot->getChild('apples')->url(), ], ], $subplugins); $warnings = file_get_contents($logfile); $this->assertMatchesRegularExpression('/No subplugintypes defined in .*subplugins.json/', $warnings); } /** * Ensure that invalid JSON in the subplugins.json file warns appropriately. * * @dataProvider invalid_subplugins_json_provider * @param string[] $expectedwarnings Errors to expect in the exception message * @param array[] $json The contents of the subplugins.json file */ public function test_fetch_subtypes_json_invalid_values( array $expectedwarnings, array $json, ): void { global $CFG; $this->resetAfterTest(); $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, [ 'plugintype' => [ 'exampleplugin' => [ 'db' => [ 'subplugins.json' => json_encode($json), 'subplugins.php' => '', ], 'apples' => [], 'pears' => [], ], ], ]); $CFG->dirroot = $vfileroot->url(); $pluginroot = $vfileroot->getChild('plugintype/exampleplugin'); $logdir = make_request_directory(); $logfile = "{$logdir}/error.log"; ini_set('error_log', $logfile); $rcm = new \ReflectionMethod(\core\component::class, 'fetch_subtypes'); $rcm->invoke(null, $pluginroot->url()); $warnings = file_get_contents($logfile); foreach ($expectedwarnings as $expectedwarning) { $this->assertMatchesRegularExpression($expectedwarning, $warnings); } } /** * Data provider for invalid subplugins.json files. * * @return array */ public static function invalid_subplugins_json_provider(): array { return [ 'Invalid characters in subtype name' => [ 'expectedwarnings' => [ "/Invalid subtype .*APPLES.*detected.*invalid characters present/", ], 'json' => [ 'subplugintypes' => [ 'APPLES' => 'plugintype/exampleplugin/apples', ], ], ], 'Subplugin which duplicates a core subsystem' => [ 'expectedwarnings' => [ "/Invalid subtype .*editor.*detected.*duplicates core subsystem/", ], 'json' => [ 'subplugintypes' => [ 'editor' => 'apples', ], ], ], 'Subplugin directory does not exist' => [ 'expectedwarnings' => [ "/Invalid subtype directory/", ], 'json' => [ 'subplugintypes' => [ 'exampleapples' => 'berries', ], ], ], 'More subplugintypes than plugintypes' => [ 'expectedwarnings' => [ "/Subplugintypes and plugintypes are not in sync/", ], 'json' => [ 'subplugintypes' => [ 'apples' => 'pears', ], 'plugintypes' => [], ], ], 'More plugintypes than subplugintypes' => [ 'expectedwarnings' => [ "/Subplugintypes and plugintypes are not in sync /", ], 'json' => [ 'subplugintypes' => [ 'apples' => 'apples', ], 'plugintypes' => [ 'apples' => 'plugintype/exampleplugin/apples', 'pears' => 'plugintype/exampleplugin/pears', ], ], ], 'subplugintype not defined in plugintype' => [ 'expectedwarnings' => [ "/Subplugintypes and plugintypes are not in sync for 'apples'/", ], 'json' => [ 'subplugintypes' => [ 'apples' => 'apples', ], 'plugintypes' => [ 'pears' => 'plugintype/exampleplugin/pears', ], ], ], 'subplugintype does not match plugintype' => [ 'expectedwarnings' => [ "/Subplugintypes and plugintypes are not in sync for 'apples'/", ], 'json' => [ 'subplugintypes' => [ 'apples' => 'apples', ], 'plugintypes' => [ 'apples' => 'plugintype/exampleplugin/pears', ], ], ], ]; } /** * Test various methods when a deprecated plugin type is introduced. * * @runInSeparateProcess * @return void */ public function test_core_component_deprecated_plugintype(): void { $this->resetAfterTest(); // Inject the 'fake' plugin type. $this->add_full_mocked_plugintype( plugintype: 'fake', path: 'lib/tests/fixtures/fakeplugins/fake' ); $componenthashbefore = component::get_all_component_hash(); $versionhashbefore = component::get_all_versions_hash(); // Deprecation-specific APIs - pre-deprecation. $this->assertArrayHasKey('fake', component::get_plugin_types()); $this->assertArrayHasKey('fullfeatured', component::get_plugin_list('fake')); $this->assertFalse(component::is_deprecated_plugin_type('fake')); $this->assertFalse(component::is_deleted_plugin_type('fake')); $this->assertFalse(component::is_plugintype_in_deprecation('fake')); $this->assertArrayNotHasKey('fake', component::get_deprecated_plugin_types()); $this->assertArrayNotHasKey('fullfeatured', component::get_deprecated_plugin_list('fake')); $this->assertArrayNotHasKey('fake', component::get_deleted_plugin_types()); $this->assertArrayNotHasKey('fullfeatured', component::get_deleted_plugin_list('fake')); $this->assertArrayHasKey('fake', component::get_all_plugin_types()); $this->assertArrayHasKey('fullfeatured', component::get_all_plugins_list('fake')); // Deprecate the fake plugintype via mocking component sources. $this->deprecate_full_mocked_plugintype('fake'); // Verify before/after hashes have changed, since the plugintype is no longer part of the hash calcs. $this->assertNotEquals(component::get_all_component_hash(), $componenthashbefore); $this->assertNotEquals(component::get_all_versions_hash(), $versionhashbefore); // Deprecation-specific APIs - post-deprecation. $this->assertTrue(component::is_deprecated_plugin_type('fake')); $this->assertFalse(component::is_deleted_plugin_type('fake')); $this->assertTrue(component::is_plugintype_in_deprecation('fake')); $this->assertArrayHasKey('fake', component::get_deprecated_plugin_types()); $this->assertArrayHasKey('fullfeatured', component::get_deprecated_plugin_list('fake')); $this->assertArrayNotHasKey('fake', component::get_deleted_plugin_types()); $this->assertArrayNotHasKey('fullfeatured', component::get_deleted_plugin_list('fake')); $this->assertArrayHasKey('fake', component::get_all_plugin_types()); $this->assertArrayHasKey('fullfeatured', component::get_all_plugins_list('fake')); // Deprecated plugins excluded from the following for B/C. $this->assertArrayNotHasKey('fake', component::get_plugin_types()); $this->assertArrayNotHasKey('fullfeatured', component::get_plugin_list('fake')); $this->assertArrayNotHasKey('fake', component::get_component_list()); $this->assertEmpty(component::get_plugin_list_with_file('fake', 'classes/dummy.php')); $this->assertEmpty(component::get_plugin_list_with_class('fake', 'dummy')); // Deprecated plugins excluded by default for B/C, but can be included by request. $this->assertNotContains('fake_fullfeatured', component::get_component_names()); $this->assertContains('fake_fullfeatured', component::get_component_names(false, true)); // Deprecated plugins included in the following. $this->assertIsString(component::get_plugin_directory('fake', 'fullfeatured')); // Used by string manager. $this->assertIsString(component::get_component_directory('fake_fullfeatured')); // Uses get_plugin_directory(). $this->assertTrue(component::has_monologo_icon('fake', 'fullfeatured')); // Uses get_plugin_directory(). $this->assertEquals('fake_fullfeatured', component::get_component_from_classname(\fake_fullfeatured\example::class)); // Class autoloading of deprecated plugins is permitted, to facilitate plugin migration code. $this->assertArrayHasKey('fake_fullfeatured\dummy', component::get_component_classes_in_namespace('fake_fullfeatured')); $this->assertTrue(class_exists(\fake_fullfeatured\dummy::class)); } /** * Test various core_component APIs when dealing with deleted plugin types. * * @runInSeparateProcess * @return void */ public function test_core_component_deleted_plugintype(): void { $this->resetAfterTest(); // Inject the 'fake' plugin type. $this->add_full_mocked_plugintype( plugintype: 'fake', path: 'lib/tests/fixtures/fakeplugins/fake', ); // Delete the fake plugintype via mocking component sources. $this->delete_full_mocked_plugintype('fake'); // Deprecation-specific methods. $this->assertFalse(component::is_deprecated_plugin_type('fake')); $this->assertTrue(component::is_deleted_plugin_type('fake')); $this->assertTrue(component::is_plugintype_in_deprecation('fake')); $this->assertArrayNotHasKey('fake', component::get_deprecated_plugin_types()); $this->assertArrayNotHasKey('fullfeatured', component::get_deprecated_plugin_list('fake')); $this->assertArrayHasKey('fake', component::get_deleted_plugin_types()); $this->assertArrayHasKey('fullfeatured', component::get_deleted_plugin_list('fake')); $this->assertArrayHasKey('fake', component::get_all_plugin_types()); $this->assertArrayHasKey('fullfeatured', component::get_all_plugins_list('fake')); // Deleted plugintypes/plugins are not included in other methods. $this->assertArrayNotHasKey('fake', component::get_plugin_types()); $this->assertArrayNotHasKey('fullfeatured', component::get_plugin_list('fake')); $this->assertNotContains('fake_fullfeatured', component::get_component_names()); $this->assertNotContains('fake_fullfeatured', component::get_component_names(false, true)); $this->assertArrayNotHasKey('fake', component::get_component_list()); $this->assertEmpty(component::get_plugin_list_with_file('fake', 'classes/dummy.php')); $this->assertEmpty(component::get_plugin_list_with_class('fake', 'dummy')); $this->assertFalse(component::has_monologo_icon('fake', 'fullfeatured')); $this->assertNull(component::get_plugin_directory('fake', 'fullfeatured')); $this->assertNull(component::get_component_directory('fake_fullfeatured')); $this->assertNull(component::get_component_from_classname(\fake_fullfeatured\example::class)); // Class autoloading of deleted plugins is not supported. $this->assertArrayNotHasKey('fake_fullfeatured\dummy', component::get_component_classes_in_namespace('fake_fullfeatured')); $this->assertFalse(class_exists(fake_fullfeatured\dummy::class)); } /** * Test various core_component APIs when dealing with deprecated subplugins. * * @runInSeparateProcess * @return void */ public function test_core_component_deprecated_subplugintype(): void { $this->resetAfterTest(); // Inject the 'fake' plugin type. This includes three mock subplugins: // 1. fullsubtype_example: a regular plugin type, not deprecated, nor deleted. // 2. fulldeprecatedsubtype_test: a deprecated subplugin type. // 3. fulldeletedsubtype_demo: a deleted subplugin type. $this->add_full_mocked_plugintype( plugintype: 'fake', path: 'lib/tests/fixtures/fakeplugins/fake', subpluginsupport: true ); $this->assert_deprecation_apis_subplugins(); } /** * Verify that a plugin which supports subplugins cannot be deprecated. * * @runInSeparateProcess * @return void */ public function test_core_component_deprecated_subplugintype_supporting_subplugins(): void { $this->resetAfterTest(); // Inject the 'fake' plugin type. This includes three mock subplugins: // 1. fullsubtype_example: a regular plugin type, not deprecated, nor deleted. // 2. fulldeprecatedsubtype_test: a deprecated subplugin type. // 3. fulldeletedsubtype_demo: a deleted subplugin type. $this->add_full_mocked_plugintype( plugintype: 'fake', path: 'lib/tests/fixtures/fakeplugins/fake', subpluginsupport: true ); // Try to deprecate the fake plugintype via mocking component sources. $this->deprecate_full_mocked_plugintype('fake'); // Deprecation unsupported, so verify core_component treats all plugins the same as before the deprecation attempt. // Debugging is expected to be emitted during core_component::init(). $this->assert_deprecation_apis_subplugins(); $this->assertDebuggingCalled('Deprecation of a plugin type which supports subplugins is not supported. ' . 'These plugin types will continue to be treated as active.', DEBUG_DEVELOPER); } /** * Verify that a plugin which supports subplugins cannot be deleted. * * @runInSeparateProcess * @return void */ public function test_core_component_deleted_subplugintype_supporting_subplugins(): void { $this->resetAfterTest(); // Inject the 'fake' plugin type. This includes three mock subplugins: // 1. fullsubtype_example: a regular plugin type, not deprecated, nor deleted. // 2. fulldeprecatedsubtype_test: a deprecated subplugin type. // 3. fulldeletedsubtype_demo: a deleted subplugin type. $this->add_full_mocked_plugintype( plugintype: 'fake', path: 'lib/tests/fixtures/fakeplugins/fake', subpluginsupport: true ); // Try to delete the fake plugintype via mocking component sources. $this->delete_full_mocked_plugintype('fake'); // Deletion unsupported, so verify core_component treats all plugins the same as before the deletion attempt. // Debugging is expected to be emitted during core_component::init(). $this->assert_deprecation_apis_subplugins(); $this->assertDebuggingCalled('Deprecation of a plugin type which supports subplugins is not supported. ' . 'These plugin types will continue to be treated as active.', DEBUG_DEVELOPER); } /** * Helper asserting the returns for various core_component APIs when dealing with deprecated and deleted subplugins. * * @return void */ protected function assert_deprecation_apis_subplugins(): void { // Deprecation-specific methods. $this->assertFalse(component::is_deprecated_plugin_type('fake')); $this->assertFalse(component::is_deprecated_plugin_type('fullsubtype')); $this->assertTrue(component::is_deprecated_plugin_type('fulldeprecatedsubtype')); $this->assertFalse(component::is_deprecated_plugin_type('fulldeletedsubtype')); $this->assertFalse(component::is_deleted_plugin_type('fake')); $this->assertFalse(component::is_deleted_plugin_type('fullsubtype')); $this->assertFalse(component::is_deleted_plugin_type('fulldeprecatedsubtype')); $this->assertTrue(component::is_deleted_plugin_type('fulldeletedsubtype')); $this->assertFalse(component::is_plugintype_in_deprecation('fake')); $this->assertFalse(component::is_plugintype_in_deprecation('fullsubtype')); $this->assertTrue(component::is_plugintype_in_deprecation('fulldeprecatedsubtype')); $this->assertTrue(component::is_plugintype_in_deprecation('fulldeletedsubtype')); $this->assertArrayNotHasKey('fake', component::get_deprecated_plugin_types()); $this->assertArrayNotHasKey('fullsubtype', component::get_deprecated_plugin_types()); $this->assertArrayHasKey('fulldeprecatedsubtype', component::get_deprecated_plugin_types()); $this->assertArrayNotHasKey('fulldeletedsubtype', component::get_deprecated_plugin_types()); $this->assertArrayNotHasKey('fullfeatured', component::get_deprecated_plugin_list('fake')); $this->assertArrayNotHasKey('example', component::get_deprecated_plugin_list('fullsubtype')); $this->assertArrayHasKey('test', component::get_deprecated_plugin_list('fulldeprecatedsubtype')); $this->assertArrayNotHasKey('demo', component::get_deprecated_plugin_list('fulldeletedsubtype')); $this->assertArrayNotHasKey('fake', component::get_deleted_plugin_types()); $this->assertArrayNotHasKey('fullsubtype', component::get_deleted_plugin_types()); $this->assertArrayNotHasKey('fulldeprecatedsubtype', component::get_deleted_plugin_types()); $this->assertArrayHasKey('fulldeletedsubtype', component::get_deleted_plugin_types()); $this->assertArrayNotHasKey('fullfeatured', component::get_deleted_plugin_list('fake')); $this->assertArrayNotHasKey('example', component::get_deleted_plugin_list('fullsubtype')); $this->assertArrayNotHasKey('test', component::get_deleted_plugin_list('fulldeprecatedsubtype')); $this->assertArrayHasKey('demo', component::get_deleted_plugin_list('fulldeletedsubtype')); $this->assertArrayHasKey('fake', component::get_all_plugin_types()); $this->assertArrayHasKey('fullsubtype', component::get_all_plugin_types()); $this->assertArrayHasKey('fulldeprecatedsubtype', component::get_all_plugin_types()); $this->assertArrayHasKey('fulldeletedsubtype', component::get_all_plugin_types()); $this->assertArrayHasKey('fullfeatured', component::get_all_plugins_list('fake')); $this->assertArrayHasKey('example', component::get_all_plugins_list('fullsubtype')); $this->assertArrayHasKey('test', component::get_all_plugins_list('fulldeprecatedsubtype')); $this->assertArrayHasKey('demo', component::get_all_plugins_list('fulldeletedsubtype')); // Deprecated and deleted plugins excluded from the following for B/C. $this->assertArrayHasKey('fake', component::get_plugin_types()); $this->assertArrayHasKey('fullsubtype', component::get_plugin_types()); $this->assertArrayNotHasKey('fulldeprecatedsubtype', component::get_plugin_types()); $this->assertArrayNotHasKey('fulldeletedsubtype', component::get_plugin_types()); $this->assertNotEmpty(component::get_plugin_list('fake')); $this->assertNotEmpty(component::get_plugin_list('fullsubtype')); $this->assertEmpty(component::get_plugin_list('fulldeprecatedsubtype')); $this->assertEmpty(component::get_plugin_list('fulldeletedsubtype')); $this->assertArrayHasKey('fake', component::get_component_list()); $this->assertArrayHasKey('fullsubtype', component::get_component_list()); $this->assertArrayNotHasKey('fulldeprecatedsubtype', component::get_component_list()); $this->assertArrayNotHasKey('fulldeletedsubtype', component::get_component_list()); $this->assertArrayHasKey('fullfeatured', component::get_plugin_list_with_file('fake', 'classes/dummy.php')); $this->assertArrayHasKey('example', component::get_plugin_list_with_file('fullsubtype', 'classes/dummy.php')); $this->assertEmpty(component::get_plugin_list_with_file('fulldeprecatedsubtype', 'classes/dummy.php')); $this->assertEmpty(component::get_plugin_list_with_file('fulldeletedsubtype', 'classes/dummy.php')); $this->assertArrayHasKey('fake_fullfeatured', component::get_plugin_list_with_class('fake', 'dummy')); $this->assertArrayHasKey('fullsubtype_example', component::get_plugin_list_with_class('fullsubtype', 'dummy')); $this->assertEmpty(component::get_plugin_list_with_class('fulldeprecatedsubtype', 'dummy')); $this->assertEmpty(component::get_plugin_list_with_class('fulldeletedsubtype', 'dummy')); $this->assertArrayHasKey('fullsubtype', component::get_subplugins('fake_fullfeatured')); $this->assertContains('example', component::get_subplugins('fake_fullfeatured')['fullsubtype']); $this->assertArrayNotHasKey('fulldeprecatedsubtype', component::get_subplugins('fake_fullfeatured')); $this->assertArrayNotHasKey('fulldeletedsubtype', component::get_subplugins('fake_fullfeatured')); $this->assertArrayHasKey('fullsubtype', component::get_all_subplugins('fake_fullfeatured')); $this->assertContains('example', component::get_all_subplugins('fake_fullfeatured')['fullsubtype']); $this->assertContains('test', component::get_all_subplugins('fake_fullfeatured')['fulldeprecatedsubtype']); $this->assertContains('demo', component::get_all_subplugins('fake_fullfeatured')['fulldeletedsubtype']); // Deprecated plugins excluded by default for B/C, but can be included by request. // Deleted plugins are always excluded. $this->assertContains('fake_fullfeatured', component::get_component_names()); $this->assertContains('fullsubtype_example', component::get_component_names()); $this->assertNotContains('fulldeprecatedsubtype_test', component::get_component_names()); $this->assertNotContains('fulldeletedsubtype_demo', component::get_component_names()); $this->assertContains('fulldeprecatedsubtype_test', component::get_component_names(false, true)); $this->assertNotContains('fulldeletedsubtype_demo', component::get_component_names(false, true)); // Deprecated plugins included in the following, but deleted plugins are excluded. $this->assertIsString(component::get_plugin_directory('fake', 'fullfeatured')); // Used by string manager. $this->assertIsString(component::get_plugin_directory('fullsubtype', 'example')); $this->assertIsString(component::get_plugin_directory('fulldeprecatedsubtype', 'test')); $this->assertNull(component::get_plugin_directory('fulldeletedsubtype', 'demo')); $this->assertIsString(component::get_component_directory('fake_fullfeatured')); // Uses get_plugin_directory(). $this->assertIsString(component::get_component_directory('fullsubtype_example')); $this->assertIsString(component::get_component_directory('fulldeprecatedsubtype_test')); $this->assertNull(component::get_component_directory('fulldeletedsubtype_demo')); $this->assertTrue(component::has_monologo_icon('fullsubtype', 'example')); // Uses get_plugin_directory(). $this->assertTrue(component::has_monologo_icon('fulldeprecatedsubtype', 'test')); $this->assertFalse(component::has_monologo_icon('fulldeletedsubtype', 'demo')); $this->assertEquals('fake_fullfeatured', component::get_component_from_classname(\fake_fullfeatured\example::class)); $this->assertEquals('fullsubtype_example', component::get_component_from_classname(\fullsubtype_example\example::class)); $this->assertEquals('fulldeprecatedsubtype_test', component::get_component_from_classname(\fulldeprecatedsubtype_test\example::class)); $this->assertNull(component::get_component_from_classname(\fulldeletedsubtype_demo\example::class)); // Deprecated and deleted plugins included in the following. $this->assertEquals('fake_fullfeatured', component::get_subtype_parent('fullsubtype')); $this->assertEquals('fake_fullfeatured', component::get_subtype_parent('fulldeprecatedsubtype')); $this->assertEquals('fake_fullfeatured', component::get_subtype_parent('fulldeletedsubtype')); // Class autoloading of deprecated plugins is permitted, to facilitate plugin migration code, but not for deleted plugins. $this->assertArrayHasKey('fake_fullfeatured\dummy', component::get_component_classes_in_namespace('fake_fullfeatured')); $this->assertArrayHasKey('fullsubtype_example\dummy', component::get_component_classes_in_namespace('fullsubtype_example')); $this->assertArrayHasKey('fulldeprecatedsubtype_test\dummy', component::get_component_classes_in_namespace('fulldeprecatedsubtype_test')); $this->assertEquals([], component::get_component_classes_in_namespace('fulldeletedsubtype_demo')); $this->assertTrue(class_exists(\fake_fullfeatured\dummy::class)); $this->assertTrue(class_exists(\fullsubtype_example\dummy::class)); $this->assertTrue(class_exists(\fulldeprecatedsubtype_test\dummy::class)); $this->assertFalse(class_exists(\fulldeletedsubtype_demo\dummy::class)); } }