diff --git a/lib/tests/component_test.php b/lib/tests/component_test.php index 917d2ad450b..0ee87636d99 100644 --- a/lib/tests/component_test.php +++ b/lib/tests/component_test.php @@ -17,6 +17,7 @@ namespace core; use core\exception\coding_exception; +use core\tests\fake_plugins_test_trait; use DirectoryIterator; use ReflectionClass; use ReflectionProperty; @@ -32,6 +33,9 @@ use ReflectionProperty; * @covers \core\component */ final class component_test extends \advanced_testcase { + + use fake_plugins_test_trait; + #[\Override] public function tearDown(): void { parent::tearDown(); @@ -1441,7 +1445,9 @@ final class component_test extends \advanced_testcase { $subplugins = $rcm->invoke(null, $pluginroot->url()); $this->assertEquals([ - 'exampleplugina' => $pluginroot->getChild('apples')->url(), + 'plugintypes' => [ + 'exampleplugina' => $pluginroot->getChild('apples')->url(), + ], ], $subplugins); } @@ -1479,7 +1485,9 @@ final class component_test extends \advanced_testcase { $subplugins = $rcm->invoke(null, $pluginroot->url()); $this->assertEquals([ - 'exampleplugina' => $pluginroot->getChild('apples')->url(), + 'plugintypes' => [ + 'exampleplugina' => $pluginroot->getChild('apples')->url(), + ], ], $subplugins); $warnings = file_get_contents($logfile); @@ -1624,4 +1632,342 @@ final class component_test extends \advanced_testcase { ], ]; } + + /** + * 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)); + } }