From 9142bd0ac6c7b94edd9968a39773e616acdd049b Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 7 Jan 2026 12:00:31 +0800 Subject: [PATCH] MDL-87716 phpunit: Update phpunit configuration for composer PHPUnit paths must be provided relative to the `phpunit.xml` file. In current operation, the `phpunit.xml` is generated in the root directory, and all paths are relative to this. When installed using Composer, the Moodle directory is in a sub-directory of the root package/directory, so the configuration must reflect this. --- phpunit.xml.dist | 384 +++++++++--------- public/admin/tool/behat/cli/init.php | 5 + .../tool/generator/cli/runtestscenario.php | 4 +- public/admin/tool/phpunit/cli/init.php | 5 + public/admin/tool/phpunit/cli/util.php | 19 +- .../classes/test/phpunit/coverage_info.php | 24 +- .../test/phpunit/exception/test_exception.php | 27 ++ .../lib/classes/test/phpunit/phpunit_util.php | 76 ++-- public/lib/classes/test/testing_util.php | 36 ++ .../classes/exception/test_exception.php | 4 +- public/lib/phpunit/tests/advanced_test.php | 2 +- public/lib/phpunit/tests/basic_test.php | 2 +- 12 files changed, 358 insertions(+), 230 deletions(-) create mode 100644 public/lib/classes/test/phpunit/exception/test_exception.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c9d9458284f..119cd316e7f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,7 +2,7 @@ - public/lib/phpunit/tests - public/lib/phpunit/tests/classes - public/lib/phpunit/tests/fixtures + @root@public/lib/phpunit/tests + @root@public/lib/phpunit/tests/classes + @root@public/lib/phpunit/tests/fixtures - public/lib/testing/tests - public/lib/testing/tests/classes - public/lib/testing/tests/fixtures + @root@public/lib/testing/tests + @root@public/lib/testing/tests/classes + @root@public/lib/testing/tests/fixtures - public/lib/ddl/tests - public/lib/ddl/tests/classes - public/lib/ddl/tests/fixtures + @root@public/lib/ddl/tests + @root@public/lib/ddl/tests/classes + @root@public/lib/ddl/tests/fixtures - public/lib/dml/tests - public/lib/dml/tests/classes - public/lib/dml/tests/fixtures + @root@public/lib/dml/tests + @root@public/lib/dml/tests/classes + @root@public/lib/dml/tests/fixtures - public/lib/tests - public/lib/tests/classes - public/lib/tests/fixtures - + @root@public/lib/tests + @root@public/lib/tests/classes + @root@public/lib/tests/fixtures + - public/lib/external/tests - public/lib/external/tests/classes - public/lib/external/tests/fixtures + @root@public/lib/external/tests + @root@public/lib/external/tests/classes + @root@public/lib/external/tests/fixtures - public/favourites/tests - public/favourites/tests/classes - public/favourites/tests/fixtures + @root@public/favourites/tests + @root@public/favourites/tests/classes + @root@public/favourites/tests/fixtures - public/lib/form/tests - public/lib/form/tests/classes - public/lib/form/tests/fixtures + @root@public/lib/form/tests + @root@public/lib/form/tests/classes + @root@public/lib/form/tests/fixtures - public/lib/filestorage/tests - public/lib/filebrowser/tests - public/files/tests - public/lib/filestorage/tests/classes - public/lib/filestorage/tests/fixtures - public/lib/filebrowser/tests/classes - public/lib/filebrowser/tests/fixtures - public/files/tests/classes - public/files/tests/fixtures + @root@public/lib/filestorage/tests + @root@public/lib/filebrowser/tests + @root@public/files/tests + @root@public/lib/filestorage/tests/classes + @root@public/lib/filestorage/tests/fixtures + @root@public/lib/filebrowser/tests/classes + @root@public/lib/filebrowser/tests/fixtures + @root@public/files/tests/classes + @root@public/files/tests/fixtures - public/filter/tests - public/filter/tests/classes - public/filter/tests/fixtures + @root@public/filter/tests + @root@public/filter/tests/classes + @root@public/filter/tests/fixtures - public/admin/roles/tests - public/admin/roles/tests/classes + @root@public/admin/roles/tests + @root@public/admin/roles/tests/classes - public/cohort/tests - public/cohort/tests/classes + @root@public/cohort/tests + @root@public/cohort/tests/classes - public/lib/grade/tests - public/grade/tests - public/grade/grading/tests - public/lib/grade/tests/classes - public/grade/tests/classes - public/grade/grading/tests/classes + @root@public/lib/grade/tests + @root@public/grade/tests + @root@public/grade/grading/tests + @root@public/lib/grade/tests/classes + @root@public/grade/tests/classes + @root@public/grade/grading/tests/classes - public/analytics/tests - public/analytics/tests/classes + @root@public/analytics/tests + @root@public/analytics/tests/classes - public/availability/tests - public/availability/tests/classes + @root@public/availability/tests + @root@public/availability/tests/classes - public/backup/controller/tests - public/backup/converter/moodle1/tests - public/backup/moodle2/tests - public/backup/tests - public/backup/util - public/backup/controller/tests/classes - public/backup/converter/moodle1/tests/classes - public/backup/moodle2/tests/classes - public/backup/tests/classes - public/backup/util/classes + @root@public/backup/controller/tests + @root@public/backup/converter/moodle1/tests + @root@public/backup/moodle2/tests + @root@public/backup/tests + @root@public/backup/util + @root@public/backup/controller/tests/classes + @root@public/backup/converter/moodle1/tests/classes + @root@public/backup/moodle2/tests/classes + @root@public/backup/tests/classes + @root@public/backup/util/classes - public/badges/tests - public/badges/tests/classes + @root@public/badges/tests + @root@public/badges/tests/classes - public/blog/tests - public/blog/tests/classes + @root@public/blog/tests + @root@public/blog/tests/classes - public/customfield/tests - public/customfield/tests/classes + @root@public/customfield/tests + @root@public/customfield/tests/classes - public/iplookup/tests - public/iplookup/tests/classes + @root@public/iplookup/tests + @root@public/iplookup/tests/classes - public/course/tests - public/course/tests/classes + @root@public/course/tests + @root@public/course/tests/classes - public/course/format/tests - public/course/format/tests/classes + @root@public/course/format/tests + @root@public/course/format/tests/classes - public/privacy/tests - public/privacy/tests/classes + @root@public/privacy/tests + @root@public/privacy/tests/classes - public/question/engine/tests - public/question/tests - public/question/type/tests - public/question/engine/upgrade/tests - public/question/engine/tests/classes - public/question/tests/classes - public/question/type/tests/classes - public/question/engine/upgrade/tests/classes + @root@public/question/engine/tests + @root@public/question/tests + @root@public/question/type/tests + @root@public/question/engine/upgrade/tests + @root@public/question/engine/tests/classes + @root@public/question/tests/classes + @root@public/question/type/tests/classes + @root@public/question/engine/upgrade/tests/classes - public/cache/tests - public/cache/tests/classes - public/cache/tests/fixtures + @root@public/cache/tests + @root@public/cache/tests/classes + @root@public/cache/tests/fixtures - public/calendar/tests - public/calendar/tests/classes - public/calendar/tests/fixtures + @root@public/calendar/tests + @root@public/calendar/tests/classes + @root@public/calendar/tests/fixtures - public/enrol/tests - public/enrol/tests/classes - public/enrol/tests/fixtures + @root@public/enrol/tests + @root@public/enrol/tests/classes + @root@public/enrol/tests/fixtures - public/group/tests - public/group/tests/classes - public/group/tests/fixtures + @root@public/group/tests + @root@public/group/tests/classes + @root@public/group/tests/fixtures - public/message/tests - public/message/tests/classes - public/message/tests/fixtures + @root@public/message/tests + @root@public/message/tests/classes + @root@public/message/tests/fixtures - public/notes/tests - public/notes/tests/classes - public/notes/tests/fixtures + @root@public/notes/tests + @root@public/notes/tests/classes + @root@public/notes/tests/fixtures - public/tag/tests - public/tag/tests/classes - public/tag/tests/fixtures + @root@public/tag/tests + @root@public/tag/tests/classes + @root@public/tag/tests/fixtures - public/rating/tests - public/rating/tests/classes - public/rating/tests/fixtures + @root@public/rating/tests + @root@public/rating/tests/classes + @root@public/rating/tests/fixtures - public/repository/tests - public/repository/tests/classes - public/repository/tests/fixtures + @root@public/repository/tests + @root@public/repository/tests/classes + @root@public/repository/tests/fixtures - public/lib/userkey/tests - public/lib/userkey/tests/classes - public/lib/userkey/tests/fixtures + @root@public/lib/userkey/tests + @root@public/lib/userkey/tests/classes + @root@public/lib/userkey/tests/fixtures - public/user/tests - public/user/tests/classes - public/user/tests/fixtures + @root@public/user/tests + @root@public/user/tests/classes + @root@public/user/tests/fixtures - public/webservice/tests - public/webservice/tests/classes - public/webservice/tests/fixtures + @root@public/webservice/tests + @root@public/webservice/tests/classes + @root@public/webservice/tests/fixtures - public/mnet/tests - public/mnet/tests/classes - public/mnet/tests/fixtures + @root@public/mnet/tests + @root@public/mnet/tests/classes + @root@public/mnet/tests/fixtures - public/completion/tests - public/completion/tests/classes - public/completion/tests/fixtures + @root@public/completion/tests + @root@public/completion/tests/classes + @root@public/completion/tests/fixtures - public/comment/tests - public/comment/tests/classes - public/comment/tests/fixtures + @root@public/comment/tests + @root@public/comment/tests/classes + @root@public/comment/tests/fixtures - public/search/tests - public/search/tests/classes - public/search/tests/fixtures + @root@public/search/tests + @root@public/search/tests/classes + @root@public/search/tests/fixtures - public/competency/tests - public/competency/tests/classes - public/competency/tests/fixtures + @root@public/competency/tests + @root@public/competency/tests/classes + @root@public/competency/tests/fixtures - public/my/tests - public/my/tests/classes - public/my/tests/fixtures + @root@public/my/tests + @root@public/my/tests/classes + @root@public/my/tests/fixtures - public/auth/tests - public/auth/tests/classes - public/auth/tests/fixtures + @root@public/auth/tests + @root@public/auth/tests/classes + @root@public/auth/tests/fixtures - public/blocks/tests - public/blocks/tests/classes - public/blocks/tests/fixtures + @root@public/blocks/tests + @root@public/blocks/tests/classes + @root@public/blocks/tests/fixtures - public/login/tests - public/login/tests/classes - public/login/tests/fixtures + @root@public/login/tests + @root@public/login/tests/classes + @root@public/login/tests/fixtures - public/plagiarism/tests - public/plagiarism/tests/classes - public/plagiarism/tests/fixtures + @root@public/plagiarism/tests + @root@public/plagiarism/tests/classes + @root@public/plagiarism/tests/fixtures - public/portfolio/tests - public/portfolio/tests/classes - public/portfolio/tests/fixtures + @root@public/portfolio/tests + @root@public/portfolio/tests/classes + @root@public/portfolio/tests/fixtures - public/lib/editor/tests - public/lib/editor/tests/classes - public/lib/editor/tests/fixtures + @root@public/lib/editor/tests + @root@public/lib/editor/tests/classes + @root@public/lib/editor/tests/fixtures - public/rss/tests - public/rss/tests/classes - public/rss/tests/fixtures + @root@public/rss/tests + @root@public/rss/tests/classes + @root@public/rss/tests/fixtures - public/lib/table/tests - public/lib/table/tests/classes - public/lib/table/tests/fixtures + @root@public/lib/table/tests + @root@public/lib/table/tests/classes + @root@public/lib/table/tests/fixtures - public/h5p/tests - public/h5p/tests/classes - public/h5p/tests/fixtures + @root@public/h5p/tests + @root@public/h5p/tests/classes + @root@public/h5p/tests/fixtures - public/lib/xapi/tests - public/lib/xapi/tests/classes - public/lib/xapi/tests/fixtures + @root@public/lib/xapi/tests + @root@public/lib/xapi/tests/classes + @root@public/lib/xapi/tests/fixtures - public/contentbank/tests - public/contentbank/tests/classes - public/contentbank/tests/fixtures + @root@public/contentbank/tests + @root@public/contentbank/tests/classes + @root@public/contentbank/tests/fixtures - public/payment/tests - public/payment/tests/classes - public/payment/tests/fixtures + @root@public/payment/tests + @root@public/payment/tests/classes + @root@public/payment/tests/fixtures - public/reportbuilder/tests - public/reportbuilder/tests/classes - public/reportbuilder/tests/fixtures + @root@public/reportbuilder/tests + @root@public/reportbuilder/tests/classes + @root@public/reportbuilder/tests/fixtures - public/admin/presets/tests - public/admin/presets/tests/classes - public/admin/presets/tests/fixtures + @root@public/admin/presets/tests + @root@public/admin/presets/tests/classes + @root@public/admin/presets/tests/fixtures - public/admin/tests - public/admin/tests/classes - public/admin/tests/fixtures + @root@public/admin/tests + @root@public/admin/tests/classes + @root@public/admin/tests/fixtures - public/communication/tests - public/communication/tests/classes - public/communication/tests/fixtures + @root@public/communication/tests + @root@public/communication/tests/classes + @root@public/communication/tests/fixtures - public/ai/tests - public/ai/tests/classes - public/ai/tests/fixtures + @root@public/ai/tests + @root@public/ai/tests/classes + @root@public/ai/tests/fixtures - public/sms/tests - public/sms/tests/classes - public/sms/tests/fixtures + @root@public/sms/tests + @root@public/sms/tests/classes + @root@public/sms/tests/fixtures |s', trim($coverages, "\n"), $data); $result = false; - if (is_writable($CFG->dirroot)) { - if ($result = file_put_contents("$CFG->root/phpunit.xml", $data)) { - testing_fix_file_permissions("$CFG->root/phpunit.xml"); + + $packageroot = self::get_package_root(); + if (is_writable($packageroot)) { + if ($result = file_put_contents("$packageroot/phpunit.xml", $data)) { + testing_fix_file_permissions("$packageroot/phpunit.xml"); } } - return (bool)$result; + return (bool) $result; } /** @@ -642,9 +652,11 @@ class phpunit_util extends \core\test\testing_util { // Use the upstream file as source for the distributed configurations. $ftemplate = file_get_contents("$CFG->root/phpunit.xml.dist"); $ftemplate = preg_replace('| *', $ftemplate); + $ftemplate = str_replace('@root@', '', $ftemplate); // Gets all the components with tests. $components = \tests_finder::get_components_with_tests('phpunit'); + $moodleroot = self::get_moodle_relative_to_root_package(); // Create the corresponding phpunit.xml file for each component. foreach ($components as $cname => $cpath) { @@ -655,7 +667,7 @@ class phpunit_util extends \core\test\testing_util { $fcontents = str_replace('', $ctemplate, $ftemplate); // Check for coverage configurations. - if ($coverageinfo = self::get_coverage_info($cpath)) { + if ($coverageinfo = self::get_coverage_info($cpath, $moodleroot)) { $coverages = self::get_coverage_config($coverageinfo->get_includelists(''), $coverageinfo->get_excludelists('')); } else { $coverages = $coveragedefault; @@ -1039,21 +1051,26 @@ class phpunit_util extends \core\test\testing_util { /** * Get the \core\test\phpunit\coverage_info for the specified plugin or subsystem directory. * - * @param string $fulldir The directory to find the coverage info file in. - * @return \core\test\phpunit\coverage_info + * @param string $fulldir The directory to find the coverage info file in + * @param string $moodleroot The base directory that Moodle is in relative to root package composer.json + * @return coverage_info */ - protected static function get_coverage_info(string $fulldir): \core\test\phpunit\coverage_info { + protected static function get_coverage_info( + string $fulldir, + string $moodleroot, + ): coverage_info { $coverageconfig = "{$fulldir}/tests/coverage.php"; if (file_exists($coverageconfig)) { $coverageinfo = require($coverageconfig); - if (!$coverageinfo instanceof \core\test\phpunit\coverage_info) { - throw new \coding_exception("{$coverageconfig} does not return a \core\test\phpunit\coverage_info"); + if (!$coverageinfo instanceof coverage_info) { + throw new \core\exception\coding_exception("{$coverageconfig} does not return a \core\test\phpunit\coverage_info"); } - - return $coverageinfo; + } else { + $coverageinfo = new coverage_info(); } - return new \core\test\phpunit\coverage_info(); + $coverageinfo->set_basedir($moodleroot); + return $coverageinfo; } /** @@ -1072,6 +1089,15 @@ class phpunit_util extends \core\test\testing_util { protected static function get_framework() { return 'phpunit'; } + + /** + * Get the path to the root of the package Moodle is installed in. + * + * @return bool|string + */ + protected static function get_package_root(): string { + return realpath(\Composer\InstalledVersions::getRootPackage()['install_path']); + } } // Alias this class to the old name. diff --git a/public/lib/classes/test/testing_util.php b/public/lib/classes/test/testing_util.php index 266ab47b00f..d02c99af67c 100644 --- a/public/lib/classes/test/testing_util.php +++ b/public/lib/classes/test/testing_util.php @@ -1020,6 +1020,42 @@ abstract class testing_util { return $env; } + + /** + * Get the path to the Moodle root, relative to the root package. + * + * @return string + */ + public static function get_moodle_relative_to_root_package(): string { + global $CFG; + + $rootpackage = \Composer\InstalledVersions::getRootPackage(); + $rootpath = realpath(\Composer\InstalledVersions::getRootPackage()['install_path']); + + $moodlepath = realpath($CFG->root); + if ($rootpath === $moodlepath) { + // Moodle is the root package. + return ''; + } + + // At the moment there is no way to get the name of the Moodle core package from the provided package. + // So we need to get all installed moodle-core packages and check which one is installed. + // It's only really possible for a single moodle-core package to be installed. + $moodlecorepackages = \Composer\InstalledVersions::getInstalledPackagesByType('moodle-core'); + if (count($moodlecorepackages) !== 1) { + throw new \core\exception\coding_exception('Unable to determine Moodle root relative to root package.'); + } + $moodlecorepackage = reset($moodlecorepackages); + if (\Composer\InstalledVersions::isInstalled($moodlecorepackage)) { + $installpath = \Composer\InstalledVersions::getInstallPath($moodlecorepackage); + if ($installpath !== null) { + // Moodle core is installed as a composer package. + return basename($installpath) . '/'; + } + } + + throw new \core\exception\coding_exception('Unable to determine Moodle root relative to root package.'); + } } // Alias this class to the old name. diff --git a/public/lib/phpunit/classes/exception/test_exception.php b/public/lib/phpunit/classes/exception/test_exception.php index c9257bd33b7..8a5eed041e2 100644 --- a/public/lib/phpunit/classes/exception/test_exception.php +++ b/public/lib/phpunit/classes/exception/test_exception.php @@ -23,5 +23,5 @@ namespace core_phpunit\exception; * @copyright Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class test_exception extends \core\exception\coding_exception { -} + +class_alias(\core\test\phpunit\exception\test_exception::class, test_exception::class); diff --git a/public/lib/phpunit/tests/advanced_test.php b/public/lib/phpunit/tests/advanced_test.php index bc99f5d0bac..93187c764d2 100644 --- a/public/lib/phpunit/tests/advanced_test.php +++ b/public/lib/phpunit/tests/advanced_test.php @@ -16,7 +16,7 @@ namespace core; -use core_phpunit\exception\test_exception; +use core\test\phpunit\exception\test_exception; /** * Test advanced_testcase extra features. diff --git a/public/lib/phpunit/tests/basic_test.php b/public/lib/phpunit/tests/basic_test.php index 05d6e3f0476..178fa5221db 100644 --- a/public/lib/phpunit/tests/basic_test.php +++ b/public/lib/phpunit/tests/basic_test.php @@ -233,7 +233,7 @@ STRING; global $DB; $DB->set_field('user', 'confirmed', 1, ['id' => -1]); - $this->expectException(\core_phpunit\exception\test_exception::class); + $this->expectException(\core\test\phpunit\exception\test_exception::class); $this->expectExceptionMessage('Warning: unexpected database modification'); phpunit_util::reset_all_data(true); }