diff --git a/lib/classes/component.php b/lib/classes/component.php index 258d7eb25a2..f4e188bbb1b 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -172,6 +172,38 @@ class core_component { require($file); return; } + + if (PHPUNIT_TEST) { + // For unit tests we support classes in `\frankenstyle_component\tests\` to be loaded from + // `path/to/frankenstyle/component/tests/classes` directory. + // Note: We do *not* support the legacy `\frankenstyle_component_tests_style_classnames`. + if ($component = self::get_component_from_classname($classname)) { + $pathoptions = [ + '/tests/classes' => "{$component}\\tests\\", + '/tests/behat' => "{$component}\\behat\\", + ]; + foreach ($pathoptions as $path => $testnamespace) { + if (preg_match("#^" . preg_quote($testnamespace) . "#", $classname)) { + $path = self::get_component_directory($component) . $path; + $relativeclassname = str_replace( + $testnamespace, + '', + $classname, + ); + $file = sprintf( + "%s/%s.php", + $path, + str_replace('\\', '/', $relativeclassname), + ); + if (!empty($file) && file_exists($file)) { + require($file); + return; + } + break; + } + } + } + } } /** diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php index 0e0bb2d1cb6..1ad2fde17a6 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php @@ -529,6 +529,7 @@ class phpunit_util extends testing_util { $template = << @dir@ + @dir@/classes EOF; @@ -621,6 +622,7 @@ class phpunit_util extends testing_util { . + ./classes EOT; diff --git a/lib/tests/component_test.php b/lib/tests/component_test.php index 74d372b3846..4a0974980d3 100644 --- a/lib/tests/component_test.php +++ b/lib/tests/component_test.php @@ -34,6 +34,14 @@ final class component_test extends advanced_testcase { */ const SUBSYSTEMCOUNT = 77; + #[\Override] + public function tearDown(): void { + parent::tearDown(); + + $plugintypes = new ReflectionProperty(\core_component::class, 'plugintypes'); + $plugintypes->setValue(null, null); + } + public function test_get_core_subsystems(): void { global $CFG; @@ -829,66 +837,66 @@ final class component_test extends advanced_testcase { '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-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' => [ @@ -914,6 +922,64 @@ final class component_test extends advanced_testcase { ]; } + /** + * 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'; + (new ReflectionProperty('core_component', 'plugintypes'))->setValue(null, null); + + // 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. * diff --git a/lib/upgrade.txt b/lib/upgrade.txt index ce3d25e93cf..fb0f2996c71 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -5,6 +5,7 @@ information provided here is intended especially for developers. * The `\core\dataformat::get_format_instance` method is now public, and can be used to retrieve a writer instance for a given dataformat +* Added the ability for unit tests to autoload classes in the `\[component]\tests\` namespace from the `[path/to/component]/tests/classes` directory. === 4.4.1 === diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fd7faeecc08..de77041fcf2 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -35,54 +35,74 @@ lib/phpunit/tests + lib/phpunit/tests/classes lib/testing/tests + lib/testing/tests/classes lib/ddl/tests + lib/ddl/tests/classes lib/dml/tests + lib/dml/tests/classes lib/tests + lib/tests/classes lib/external/tests + lib/external/tests/classes favourites/tests + favourites/tests/classes lib/form/tests + lib/form/tests/classes lib/filestorage/tests lib/filebrowser/tests files/tests + lib/filestorage/tests/classes + lib/filebrowser/tests/classes + files/tests/classes filter/tests + filter/tests/classes admin/roles/tests + admin/roles/tests/classes cohort/tests + cohort/tests/classes lib/grade/tests grade/tests grade/grading/tests grade/import/csv/tests + lib/grade/tests/classes + grade/tests/classes + grade/grading/tests/classes + grade/import/csv/tests/classes analytics/tests + analytics/tests/classes availability/tests + availability/tests/classes backup/controller/tests @@ -90,135 +110,185 @@ backup/moodle2/tests backup/tests backup/util + backup/controller/tests/classes + backup/converter/moodle1/tests/classes + backup/moodle2/tests/classes + backup/tests/classes + backup/util/classes badges/tests + badges/tests/classes blog/tests + blog/tests/classes customfield/tests + customfield/tests/classes iplookup/tests + iplookup/tests/classes course/tests + course/tests/classes course/format/tests + course/format/tests/classes privacy/tests + privacy/tests/classes question/engine/tests question/tests question/type/tests question/engine/upgrade/tests + question/engine/tests/classes + question/tests/classes + question/type/tests/classes + question/engine/upgrade/tests/classes cache/tests + cache/tests/classes calendar/tests + calendar/tests/classes enrol/tests + enrol/tests/classes group/tests + group/tests/classes message/tests + message/tests/classes notes/tests + notes/tests/classes tag/tests + tag/tests/classes rating/tests + rating/tests/classes repository/tests + repository/tests/classes lib/userkey/tests + lib/userkey/tests/classes user/tests + user/tests/classes webservice/tests + webservice/tests/classes mnet/tests + mnet/tests/classes completion/tests + completion/tests/classes comment/tests + comment/tests/classes search/tests + search/tests/classes competency/tests + competency/tests/classes my/tests + my/tests/classes auth/tests + auth/tests/classes blocks/tests + blocks/tests/classes login/tests + login/tests/classes plagiarism/tests + plagiarism/tests/classes portfolio/tests + portfolio/tests/classes lib/editor/tests + lib/editor/tests/classes rss/tests + rss/tests/classes lib/table/tests + lib/table/tests/classes h5p/tests + h5p/tests/classes lib/xapi/tests + lib/xapi/tests/classes contentbank/tests + contentbank/tests/classes payment/tests + payment/tests/classes reportbuilder/tests + reportbuilder/tests/classes admin/presets/tests + admin/presets/tests/classes admin/tests + admin/tests/classes communication/tests + communication/tests/classes