From 429eeaafabf123502f352f89f8760513ceb0cde0 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 28 Jan 2026 16:19:47 +0800 Subject: [PATCH 1/7] MDL-87716 core: Update namespace for various phpunit classes --- .upgradenotes/MDL-87716-2026012808042644.yml | 18 + .../tests/behat/behat_tool_dataprivacy.php | 6 +- .../tool/generator/classes/course_backend.php | 13 +- .../tests/behat/behat_tool_lp_deprecated.php | 9 +- .../monitor/tests/eventobservers_test.php | 8 +- public/admin/tool/phpunit/cli/util.php | 2 + .../tests/classes/mlbackend_helper_trait.php | 8 +- public/analytics/tests/prediction_test.php | 21 +- public/backup/util/xml/tests/writer_test.php | 3 +- public/blocks/tests/coverage.php | 4 +- .../tests/cachestore_cluster_redis_test.php | 2 +- public/competency/tests/generator/lib.php | 4 +- public/completion/tests/coverage.php | 13 +- .../format/tests/local/cmactions_test.php | 2 +- .../tests/local/sectionactions_test.php | 2 +- public/course/tests/courselib_test.php | 4 +- public/course/tests/coverage.php | 2 +- .../nofactivities/nofactivities_test.php | 2 +- .../task/sync_tool_grades_test.php | 2 - .../lib/behat/classes/behat_config_util.php | 5 +- public/lib/behat/classes/util.php | 9 +- public/lib/classes/event/base.php | 6 +- .../classes/test/phpunit/coverage_info.php | 108 ++ .../lib/classes/test/phpunit/event_mock.php | 52 + .../lib/classes/test/phpunit/event_sink.php | 84 ++ .../lib/classes/test/phpunit/message_sink.php | 116 ++ .../classes/test/phpunit/phpmailer_sink.php | 84 ++ .../lib/classes/test/phpunit/phpunit_util.php | 1080 +++++++++++++++++ public/lib/classes/test/test_finder.php | 204 ++++ .../test/testing_util.php} | 86 +- public/lib/db/legacyclasses.php | 9 + public/lib/dml/moodle_database.php | 1 - public/lib/messagelib.php | 8 +- public/lib/moodlelib.php | 2 +- public/lib/phpmailer/moodle_phpmailer.php | 6 +- public/lib/phpunit/bootstrap.php | 6 +- .../lib/phpunit/classes/advanced_testcase.php | 17 +- public/lib/phpunit/classes/basic_testcase.php | 7 +- public/lib/phpunit/classes/coverage_info.php | 84 +- .../classes/database_driver_testcase.php | 7 +- public/lib/phpunit/classes/event_mock.php | 38 +- public/lib/phpunit/classes/event_sink.php | 68 +- public/lib/phpunit/classes/message_sink.php | 100 +- public/lib/phpunit/classes/phpmailer_sink.php | 68 +- .../lib/phpunit/classes/phpunit_dataset.php | 3 +- public/lib/phpunit/classes/util.php | 1064 +--------------- public/lib/phpunit/lib.php | 6 - public/lib/phpunit/tests/advanced_test.php | 10 +- public/lib/phpunit/tests/basic_test.php | 4 +- public/lib/phpunit/tests/util_test.php | 8 +- public/lib/setup.php | 6 +- public/lib/testing/classes/tests_finder.php | 188 +-- .../data_provider_finished_subscriber.php | 2 +- public/lib/tests/coverage.php | 11 +- .../lib/tests/curl_security_helper_test.php | 22 +- public/lib/tests/message_test.php | 4 +- .../mustache_template_source_loader_test.php | 4 +- public/lib/tests/scss_test.php | 2 +- public/lib/weblib.php | 2 +- public/lib/xhprof/xhprof_moodle.php | 3 +- .../message/output/popup/tests/api_test.php | 2 +- .../output/popup/tests/externallib_test.php | 2 +- public/mod/assign/tests/locallib_test.php | 2 +- .../tests/backup_restore_test.php | 2 +- public/mod/bigbluebuttonbn/tests/coverage.php | 17 +- public/mod/data/tests/generator/lib.php | 12 - public/mod/forum/tests/generator/lib.php | 1 - public/mod/forum/tests/mail_group_test.php | 2 +- public/mod/forum/tests/mail_test.php | 6 +- public/mod/forum/tests/maildigest_test.php | 4 +- public/mod/forum/tests/qanda_test.php | 2 +- .../tests/moodle_content_writer_test.php | 34 +- public/question/engine/tests/coverage.php | 5 +- public/question/engine/tests/helpers.php | 6 +- .../aiken/tests/qformat_aiken_export_test.php | 4 +- .../format/gift/tests/giftformat_test.php | 2 +- .../tests/qformat_xml_import_export_test.php | 3 +- public/question/tests/coverage.php | 4 +- .../type/numerical/tests/coverage.php | 4 +- .../question/type/ordering/tests/coverage.php | 4 +- .../type/ordering/tests/questiontype_test.php | 3 +- .../repository/nextcloud/tests/lib_test.php | 5 +- public/user/tests/coverage.php | 2 +- 83 files changed, 2015 insertions(+), 1832 deletions(-) create mode 100644 .upgradenotes/MDL-87716-2026012808042644.yml create mode 100644 public/lib/classes/test/phpunit/coverage_info.php create mode 100644 public/lib/classes/test/phpunit/event_mock.php create mode 100644 public/lib/classes/test/phpunit/event_sink.php create mode 100644 public/lib/classes/test/phpunit/message_sink.php create mode 100644 public/lib/classes/test/phpunit/phpmailer_sink.php create mode 100644 public/lib/classes/test/phpunit/phpunit_util.php create mode 100644 public/lib/classes/test/test_finder.php rename public/lib/{testing/classes/util.php => classes/test/testing_util.php} (93%) diff --git a/.upgradenotes/MDL-87716-2026012808042644.yml b/.upgradenotes/MDL-87716-2026012808042644.yml new file mode 100644 index 00000000000..3ece35f222b --- /dev/null +++ b/.upgradenotes/MDL-87716-2026012808042644.yml @@ -0,0 +1,18 @@ +issueNumber: MDL-87716 +notes: + core: + - message: | + The following classes have been renamed and now support autoloading. + + The old names will be maintained without debugging until Moodle 6.0. + + | Old class name | New class name | + | --- | --- | + | `\phpunit_coverage_info` | `\core\test\phpunit\coverage_info` | + | `\phpunit_event_mock` | `\core\test\phpunit\event_mock` | + | `\phpunit_event_sink` | `\core\test\phpunit\event_sink` | + | `\phpunit_message_sink` | `\core\test\phpunit\message_sink` | + | `\phpunit_phpmailer_sink` | `\core\test\phpunit\phpmailer_sink` | + | `\phpunit_util` | `\core\test\phpunit\phpunit_util` | + | `\testing_util` | `\core\test\testing_util` | + type: changed diff --git a/public/admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php b/public/admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php index 6366df4b210..af1cf39fe99 100644 --- a/public/admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php +++ b/public/admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php @@ -66,15 +66,11 @@ class behat_tool_dataprivacy extends behat_base { * @param TableNode $data */ public function the_following_data_categories_exist($elementname, TableNode $data) { - - // Now that we need them require the data generators. - require_once(__DIR__.'/../../../../../lib/phpunit/classes/util.php'); - if (empty(self::$elements[$elementname])) { throw new PendingException($elementname . ' data generator is not implemented'); } - $datagenerator = testing_util::get_data_generator(); + $datagenerator = \core\test\testing_util::get_data_generator(); $dataprivacygenerator = $datagenerator->get_plugin_generator('tool_dataprivacy'); $elementdatagenerator = self::$elements[$elementname]['datagenerator']; diff --git a/public/admin/tool/generator/classes/course_backend.php b/public/admin/tool/generator/classes/course_backend.php index 820f1d704be..b33cad0423e 100644 --- a/public/admin/tool/generator/classes/course_backend.php +++ b/public/admin/tool/generator/classes/course_backend.php @@ -14,16 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * tool_generator course backend code. - * - * @package tool_generator - * @copyright 2013 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die(); - /** * Backend code for the 'make large course' tool. * @@ -217,7 +207,6 @@ class tool_generator_course_backend extends tool_generator_backend { */ public function make() { global $DB, $CFG, $USER; - require_once($CFG->dirroot . '/lib/phpunit/classes/util.php'); raise_memory_limit(MEMORY_EXTRA); @@ -228,7 +217,7 @@ class tool_generator_course_backend extends tool_generator_backend { $entirestart = microtime(true); // Get generator. - $this->generator = phpunit_util::get_data_generator(); + $this->generator = \core\test\phpunit\phpunit_util::get_data_generator(); // Make course. $this->course = $this->create_course(); diff --git a/public/admin/tool/lp/tests/behat/behat_tool_lp_deprecated.php b/public/admin/tool/lp/tests/behat/behat_tool_lp_deprecated.php index 0ee66509f99..6c45fd81f05 100644 --- a/public/admin/tool/lp/tests/behat/behat_tool_lp_deprecated.php +++ b/public/admin/tool/lp/tests/behat/behat_tool_lp_deprecated.php @@ -25,8 +25,8 @@ require_once(__DIR__ . '/../../../../../lib/behat/behat_deprecated_base.php'); -use Behat\Gherkin\Node\TableNode as TableNode; -use Behat\Behat\Tester\Exception\PendingException as PendingException; +use Behat\Gherkin\Node\TableNode; +use Behat\Behat\Tester\Exception\PendingException; use core_competency\competency; use core_competency\competency_framework; use core_competency\plan; @@ -114,14 +114,11 @@ class behat_tool_lp_deprecated extends behat_deprecated_base { ]); - // Now that we need them require the data generators. - require_once(__DIR__.'/../../../../../lib/phpunit/classes/util.php'); - if (empty(self::$elements[$elementname])) { throw new PendingException($elementname . ' data generator is not implemented'); } - $datagenerator = testing_util::get_data_generator(); + $datagenerator = \core\test\testing_util::get_data_generator(); $this->datageneratorlp = $datagenerator->get_plugin_generator('core_competency'); $elementdatagenerator = self::$elements[$elementname]['datagenerator']; diff --git a/public/admin/tool/monitor/tests/eventobservers_test.php b/public/admin/tool/monitor/tests/eventobservers_test.php index 252378263bb..82ef509de39 100644 --- a/public/admin/tool/monitor/tests/eventobservers_test.php +++ b/public/admin/tool/monitor/tests/eventobservers_test.php @@ -411,9 +411,9 @@ final class eventobservers_test extends \advanced_testcase { /** * Verify that task was scheduled and a message was sent as expected. * - * @param phpunit_message_sink $msgsink Message sink + * @param \core\test\phpunit\message_sink $msgsink Message sink */ - protected function verify_processed_data(\phpunit_message_sink $msgsink) { + protected function verify_processed_data(\core\test\phpunit\message_sink $msgsink) { global $DB, $USER; $recordexists = $DB->count_records('task_adhoc', array('component' => 'tool_monitor')); @@ -430,9 +430,9 @@ final class eventobservers_test extends \advanced_testcase { /** * Verify that a message was not sent. * - * @param phpunit_message_sink $msgsink Message sink + * @param \core\test\phpunit\message_sink $msgsink Message sink */ - protected function verify_message_not_sent_yet(\phpunit_message_sink $msgsink) { + protected function verify_message_not_sent_yet(\core\test\phpunit\message_sink $msgsink) { $msgs = $msgsink->get_messages(); $this->assertCount(0, $msgs); $msgsink->clear(); diff --git a/public/admin/tool/phpunit/cli/util.php b/public/admin/tool/phpunit/cli/util.php index ff5eb956ddd..41053117d03 100644 --- a/public/admin/tool/phpunit/cli/util.php +++ b/public/admin/tool/phpunit/cli/util.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +use core\test\phpunit\phpunit_util; + /** * PHPUnit related utilities. * diff --git a/public/analytics/tests/classes/mlbackend_helper_trait.php b/public/analytics/tests/classes/mlbackend_helper_trait.php index eaa33974e6f..6f347482c9d 100644 --- a/public/analytics/tests/classes/mlbackend_helper_trait.php +++ b/public/analytics/tests/classes/mlbackend_helper_trait.php @@ -16,8 +16,6 @@ namespace core_analytics\tests; -use phpunit_util; - /** * A trait to check machine learning configurations. * @@ -59,18 +57,18 @@ trait mlbackend_helper_trait { for ($i = 0; $i < $ncourses; $i++) { $name = 'a' . random_string(10); $courseparams = ['shortname' => $name, 'fullname' => $name] + $params; - phpunit_util::get_data_generator()->create_course($courseparams); + \core\test\phpunit\phpunit_util::get_data_generator()->create_course($courseparams); } for ($i = 0; $i < $ncourses; $i++) { $name = 'b' . random_string(10); $courseparams = ['shortname' => $name, 'fullname' => $name] + $params; - phpunit_util::get_data_generator()->create_course($courseparams); + \core\test\phpunit\phpunit_util::get_data_generator()->create_course($courseparams); } if ($ismulticlass) { for ($i = 0; $i < $ncourses; $i++) { $name = 'c' . random_string(10); $courseparams = ['shortname' => $name, 'fullname' => $name] + $params; - phpunit_util::get_data_generator()->create_course($courseparams); + \core\test\phpunit\phpunit_util::get_data_generator()->create_course($courseparams); } } } diff --git a/public/analytics/tests/prediction_test.php b/public/analytics/tests/prediction_test.php index ddbfb933a39..706b702761e 100644 --- a/public/analytics/tests/prediction_test.php +++ b/public/analytics/tests/prediction_test.php @@ -16,6 +16,8 @@ namespace core_analytics; +use core_analytics\tests\mlbackend_helper_trait; + defined('MOODLE_INTERNAL') || die(); global $CFG; @@ -28,11 +30,8 @@ require_once(__DIR__ . '/fixtures/test_indicator_multiclass.php'); require_once(__DIR__ . '/fixtures/test_target_shortname.php'); require_once(__DIR__ . '/fixtures/test_target_shortname_multiclass.php'); require_once(__DIR__ . '/fixtures/test_static_target_shortname.php'); - require_once(__DIR__ . '/../../course/lib.php'); -use core_analytics\tests\mlbackend_helper_trait; - /** * Unit tests for evaluation, training and prediction. * @@ -745,8 +744,12 @@ final class prediction_test extends \advanced_testcase { $samples, $ranges ); - $dataset = \phpunit_util::call_internal_method($analysis, 'calculate_indicators', $params, - '\core_analytics\analysis'); + $dataset = \core\test\phpunit\phpunit_util::call_internal_method( + $analysis, + 'calculate_indicators', + $params, + \core_analytics\analysis::class, + ); $this->assertArrayHasKey('123-0', $dataset); $this->assertArrayHasKey('123-1', $dataset); $this->assertArrayHasKey('321-0', $dataset); @@ -769,8 +772,12 @@ final class prediction_test extends \advanced_testcase { $samples, $ranges ); - $dataset = \phpunit_util::call_internal_method($analysis, 'calculate_indicators', $params, - '\core_analytics\analysis'); + $dataset = \core\test\phpunit\phpunit_util::call_internal_method( + $analysis, + 'calculate_indicators', + $params, + \core_analytics\analysis::class, + ); $this->assertArrayNotHasKey('123-0', $dataset); $this->assertArrayNotHasKey('123-1', $dataset); $this->assertArrayNotHasKey('321-0', $dataset); diff --git a/public/backup/util/xml/tests/writer_test.php b/public/backup/util/xml/tests/writer_test.php index f7533744643..ebb5adaca0b 100644 --- a/public/backup/util/xml/tests/writer_test.php +++ b/public/backup/util/xml/tests/writer_test.php @@ -26,7 +26,6 @@ namespace core_backup; use memory_xml_output; -use phpunit_util; use xml_contenttransformer; use xml_output; use xml_writer; @@ -333,7 +332,7 @@ final class writer_test extends \basic_testcase { $fcontents = file_get_contents($CFG->dirroot . '/backup/util/xml/tests/fixtures/test1.xml'); // Normalise carriage return characters. - $fcontents = phpunit_util::normalise_line_endings($fcontents); + $fcontents = \core\test\phpunit\phpunit_util::normalise_line_endings($fcontents); $this->assertEquals(trim($result), trim($fcontents)); } } diff --git a/public/blocks/tests/coverage.php b/public/blocks/tests/coverage.php index 8089444dfe5..471210f3062 100644 --- a/public/blocks/tests/coverage.php +++ b/public/blocks/tests/coverage.php @@ -17,10 +17,12 @@ /** * Coverage information for the core_block subsystem. * + * @package core + * @category phpunit * @copyright 2021 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -return new class extends phpunit_coverage_info { +return new class extends \core\test\phpunit\coverage_info { /** @var array The list of files relative to the plugin root to include in coverage generation. */ protected $includelistfiles = [ 'moodleblock.class.php', diff --git a/public/cache/stores/redis/tests/cachestore_cluster_redis_test.php b/public/cache/stores/redis/tests/cachestore_cluster_redis_test.php index d8047dd6d96..e0f5217c942 100644 --- a/public/cache/stores/redis/tests/cachestore_cluster_redis_test.php +++ b/public/cache/stores/redis/tests/cachestore_cluster_redis_test.php @@ -187,7 +187,7 @@ final class cachestore_cluster_redis_test extends \advanced_testcase { $store = new cachestore_redis('TestCluster', $config); $debugging = $this->getDebuggingMessages(); // Failed to connect should show a debugging message. - $this->assertCount(1, \phpunit_util::get_debugging_messages() ); + $this->assertCount(1, \core\test\phpunit\phpunit_util::get_debugging_messages() ); $this->assertStringContainsString('Couldn\'t map cluster keyspace using any provided seed', $debugging[0]->message); $this->resetDebugging(); $this->assertFalse($store->is_ready()); diff --git a/public/competency/tests/generator/lib.php b/public/competency/tests/generator/lib.php index e73957cf417..b52fa3ad107 100644 --- a/public/competency/tests/generator/lib.php +++ b/public/competency/tests/generator/lib.php @@ -28,7 +28,6 @@ use core_competency\competency_framework; use core_competency\course_competency; use core_competency\course_module_competency; use core_competency\evidence; -use core_competency\external; use core_competency\plan; use core_competency\plan_competency; use core_competency\related_competency; @@ -41,7 +40,6 @@ use core_competency\user_competency_plan; use core_competency\user_evidence; use core_competency\user_evidence_competency; - defined('MOODLE_INTERNAL') || die(); global $CFG; @@ -139,7 +137,7 @@ class core_competency_generator extends component_generator_base { if (defined('BEHAT_TEST') && BEHAT_TEST) { $generator = behat_util::get_data_generator(); } else { - $generator = phpunit_util::get_data_generator(); + $generator = \core\test\phpunit\phpunit_util::get_data_generator(); } $this->frameworkcount++; $i = $this->frameworkcount; diff --git a/public/completion/tests/coverage.php b/public/completion/tests/coverage.php index 2a758a79d66..73d1319f548 100644 --- a/public/completion/tests/coverage.php +++ b/public/completion/tests/coverage.php @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -defined('MOODLE_INTERNAL') || die(); - /** * Coverage information for the core_completion. * @@ -24,14 +22,7 @@ defined('MOODLE_INTERNAL') || die(); * @copyright 2022 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -/** - * Coverage information for the core subsystem. - * - * @copyright 2018 Andrew Nicols - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -return new class extends phpunit_coverage_info { +return new class extends \core\test\phpunit\coverage_info { /** @var array The list of folders relative to the plugin root to include in coverage generation. */ protected $includelistfolders = [ 'criteria', @@ -42,6 +33,6 @@ return new class extends phpunit_coverage_info { 'completion_aggregation.php', 'completion_completion.php', 'completion_criteria_completion.php', - 'data_object.php' + 'data_object.php', ]; }; diff --git a/public/course/format/tests/local/cmactions_test.php b/public/course/format/tests/local/cmactions_test.php index f137a529853..200da89434c 100644 --- a/public/course/format/tests/local/cmactions_test.php +++ b/public/course/format/tests/local/cmactions_test.php @@ -465,7 +465,7 @@ final class cmactions_test extends \advanced_testcase { $sink = $this->redirectEvents(); // Now, run the adhoc task which performs the hard deletion. - \phpunit_util::run_all_adhoc_tasks(); + \core\test\phpunit\phpunit_util::run_all_adhoc_tasks(); // Fetch and validate the event data. $events = $sink->get_events(); diff --git a/public/course/format/tests/local/sectionactions_test.php b/public/course/format/tests/local/sectionactions_test.php index 5a4b75e530a..40ef3dfb139 100644 --- a/public/course/format/tests/local/sectionactions_test.php +++ b/public/course/format/tests/local/sectionactions_test.php @@ -508,7 +508,7 @@ final class sectionactions_test extends \advanced_testcase { // Now, run the adhoc task to delete the modules from section 0. $sink = $this->redirectEvents(); // To capture the events. - \phpunit_util::run_all_adhoc_tasks(); + \core\test\phpunit\phpunit_util::run_all_adhoc_tasks(); // Confirm the modules have been deleted. list($insql, $assignids) = $DB->get_in_or_equal([$assign0->cmid, $assign1->cmid, $assign2->cmid]); diff --git a/public/course/tests/courselib_test.php b/public/course/tests/courselib_test.php index 3fa7eeb9cdc..6ae10c08543 100644 --- a/public/course/tests/courselib_test.php +++ b/public/course/tests/courselib_test.php @@ -29,7 +29,6 @@ use context_course; use context_module; use context_system; use context_coursecat; -use core\event\section_viewed; use core_completion_external; use core_courseformat\formatactions; use core_external; @@ -43,7 +42,6 @@ use grade_item; use grading_manager; use moodle_exception; use moodle_url; -use phpunit_util; use rating_manager; use restore_controller; use stdClass; @@ -3880,7 +3878,7 @@ final class courselib_test extends advanced_testcase { // Now, run the adhoc task to delete the modules from section 0. $sink = $this->redirectEvents(); // To capture the events. - phpunit_util::run_all_adhoc_tasks(); + \core\test\phpunit\phpunit_util::run_all_adhoc_tasks(); // Confirm the modules have been deleted. list($insql, $assignids) = $DB->get_in_or_equal([$assign0->cmid, $assign1->cmid, $assign2->cmid]); diff --git a/public/course/tests/coverage.php b/public/course/tests/coverage.php index 324a3a630a5..3e5683aea6d 100644 --- a/public/course/tests/coverage.php +++ b/public/course/tests/coverage.php @@ -20,7 +20,7 @@ * @copyright 2021 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -return new class extends phpunit_coverage_info { +return new class extends \core\test\phpunit\coverage_info { /** @var array The list of files relative to the plugin root to include in coverage generation. */ protected $includelistfiles = [ 'dnduploadlib.php', diff --git a/public/customfield/field/number/tests/local/numberproviders/nofactivities/nofactivities_test.php b/public/customfield/field/number/tests/local/numberproviders/nofactivities/nofactivities_test.php index 24e14b57901..443e8a40184 100644 --- a/public/customfield/field/number/tests/local/numberproviders/nofactivities/nofactivities_test.php +++ b/public/customfield/field/number/tests/local/numberproviders/nofactivities/nofactivities_test.php @@ -108,7 +108,7 @@ final class nofactivities_test extends advanced_testcase { $this->assertEquals(3.0000, $course1customfield); // Now, run the course module deletion adhoc task. - \phpunit_util::run_all_adhoc_tasks(); + \core\test\phpunit\phpunit_util::run_all_adhoc_tasks(); $providers[0]->recalculate((int)$course1->id); $course1customfield = $DB->get_field('customfield_data', 'decvalue', ['instanceid' => $course1->id]); $this->assertEquals(3.0000, $course1customfield); diff --git a/public/enrol/lti/tests/local/ltiadvantage/task/sync_tool_grades_test.php b/public/enrol/lti/tests/local/ltiadvantage/task/sync_tool_grades_test.php index 98ac824c807..82f76770840 100644 --- a/public/enrol/lti/tests/local/ltiadvantage/task/sync_tool_grades_test.php +++ b/public/enrol/lti/tests/local/ltiadvantage/task/sync_tool_grades_test.php @@ -20,8 +20,6 @@ use enrol_lti\helper; use Packback\Lti1p3\LtiAssignmentsGradesService; use Packback\Lti1p3\LtiGrade; use Packback\Lti1p3\LtiLineitem; -use core\task\manager; -use phpunit_util; defined('MOODLE_INTERNAL') || die(); diff --git a/public/lib/behat/classes/behat_config_util.php b/public/lib/behat/classes/behat_config_util.php index 8b535d2e98a..df3a23a5cc5 100644 --- a/public/lib/behat/classes/behat_config_util.php +++ b/public/lib/behat/classes/behat_config_util.php @@ -26,7 +26,6 @@ defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../lib.php'); require_once(__DIR__ . '/behat_command.php'); -require_once(__DIR__ . '/../../testing/classes/tests_finder.php'); /** * Behat configuration manager @@ -1064,7 +1063,7 @@ class behat_config_util { /** * Cleans the path returned by get_components_with_tests() to standarize it * - * @see tests_finder::get_all_directories_with_tests() it returns the path including /tests/ + * @see \core\test\test_finder::get_all_directories_with_tests() it returns the path including /tests/ * @param string $path * @return string The string without the last /tests part */ @@ -1127,7 +1126,7 @@ class behat_config_util { */ protected function get_components_with_tests() { if (empty($this->componentswithtests)) { - $this->componentswithtests = tests_finder::get_components_with_tests('behat'); + $this->componentswithtests = \core\test\test_finder::get_components_with_tests('behat'); } return $this->componentswithtests; diff --git a/public/lib/behat/classes/util.php b/public/lib/behat/classes/util.php index 83c64db4f26..9c8e54c21c4 100644 --- a/public/lib/behat/classes/util.php +++ b/public/lib/behat/classes/util.php @@ -26,7 +26,6 @@ defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../lib.php'); -require_once(__DIR__ . '/../../testing/classes/util.php'); require_once(__DIR__ . '/behat_command.php'); require_once(__DIR__ . '/behat_config_manager.php'); @@ -45,7 +44,7 @@ use Behat\Mink\Exception\ExpectationException; * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class behat_util extends testing_util { +class behat_util extends \core\test\testing_util { /** * The behat test site fullname and shortname. @@ -531,4 +530,10 @@ EOF; return $siteinfo; } + + + #[\Override] + protected static function get_framework() { + return 'behat'; + } } diff --git a/public/lib/classes/event/base.php b/public/lib/classes/event/base.php index e212054214b..10d33e0e6c0 100644 --- a/public/lib/classes/event/base.php +++ b/public/lib/classes/event/base.php @@ -16,8 +16,6 @@ namespace core\event; -defined('MOODLE_INTERNAL') || die(); - /** * Base event class. * @@ -786,9 +784,9 @@ abstract class base implements \IteratorAggregate { $this->triggered = true; - if (PHPUNIT_TEST and \phpunit_util::is_redirecting_events()) { + if (PHPUNIT_TEST && \core\test\phpunit\phpunit_util::is_redirecting_events()) { $this->dispatched = true; - \phpunit_util::event_triggered($this); + \core\test\phpunit\phpunit_util::event_triggered($this); return; } diff --git a/public/lib/classes/test/phpunit/coverage_info.php b/public/lib/classes/test/phpunit/coverage_info.php new file mode 100644 index 00000000000..844e7557992 --- /dev/null +++ b/public/lib/classes/test/phpunit/coverage_info.php @@ -0,0 +1,108 @@ +. + +namespace core\test\phpunit; + +use core\output\html_writer; + +/** + * Coverage information for PHPUnit. + * + * @package core + * @category phpunit + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class coverage_info { + + /** @var array The list of folders relative to the plugin root to include in coverage generation. */ + protected $includelistfolders = []; + + /** @var array The list of files relative to the plugin root to include in coverage generation. */ + protected $includelistfiles = []; + + /** @var array The list of folders relative to the plugin root to exclude from coverage generation. */ + protected $excludelistfolders = []; + + /** @var array The list of files relative to the plugin root to exclude from coverage generation. */ + protected $excludelistfiles = []; + + /** + * Get the formatted XML list of files and folders to include. + * + * @param string $plugindir The root of the plugin, relative to the dataroot. + * @return array + */ + final public function get_includelists(string $plugindir): array { + $coverages = []; + + $includelistfolders = array_merge([ + 'classes', + 'tests/generator', + ], $this->includelistfolders);; + + $includelistfiles = array_merge([ + 'externallib.php', + 'lib.php', + 'locallib.php', + 'renderer.php', + 'rsslib.php', + ], $this->includelistfiles); + + if (!empty($plugindir)) { + $plugindir .= "/"; + } + + foreach (array_unique($includelistfolders) as $folder) { + $coverages[] = html_writer::tag('directory', "{$plugindir}{$folder}", ['suffix' => '.php']); + } + + foreach (array_unique($includelistfiles) as $file) { + $coverages[] = html_writer::tag('file', "{$plugindir}{$file}"); + } + + return $coverages; + } + + /** + * Get the formatted XML list of files and folders to exclude. + * + * @param string $plugindir The root of the plugin, relative to the dataroot. + * @return array + */ + final public function get_excludelists(string $plugindir): array { + $coverages = []; + + if (!empty($plugindir)) { + $plugindir .= "/"; + } + + foreach ($this->excludelistfolders as $folder) { + $coverages[] = html_writer::tag('directory', "{$plugindir}{$folder}", ['suffix' => '.php']); + } + + foreach ($this->excludelistfiles as $file) { + $coverages[] = html_writer::tag('file', "{$plugindir}{$file}"); + } + + return $coverages; + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(coverage_info::class, \phpunit_coverage_info::class); diff --git a/public/lib/classes/test/phpunit/event_mock.php b/public/lib/classes/test/phpunit/event_mock.php new file mode 100644 index 00000000000..eddbde23057 --- /dev/null +++ b/public/lib/classes/test/phpunit/event_mock.php @@ -0,0 +1,52 @@ +. + +namespace core\test\phpunit; + +/** + * Event mock class. + * + * @package core + * @category phpunit + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class event_mock extends \core\event\base { + /** + * Returns event context. + * + * @param \core\event\base $event event to get context for. + * @return \core\context event context + */ + public static function testable_get_event_context($event) { + return $event->context; + } + + /** + * Sets event context. + * + * @param \core\event\base $event event to set context for. + * @param \core\context $context context to set. + */ + public static function testable_set_event_context($event, $context) { + $event->context = $context; + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(event_mock::class, \phpunit_event_mock::class); diff --git a/public/lib/classes/test/phpunit/event_sink.php b/public/lib/classes/test/phpunit/event_sink.php new file mode 100644 index 00000000000..d24aaf7fea7 --- /dev/null +++ b/public/lib/classes/test/phpunit/event_sink.php @@ -0,0 +1,84 @@ +. + +namespace core\test\phpunit; + +/** + * Event redirection sink. + * + * @package core + * @category phpunit + * @copyright 2013 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class event_sink { + /** @var \core\event\base[] array of events */ + protected $events = array(); + + /** + * Stop event redirection. + * + * Use if you do not want event redirected any more. + */ + public function close() { + phpunit_util::stop_event_redirection(); + } + + /** + * To be called from phpunit_util only! + * + * @private + * @param \core\event\base $event record from event_read table + */ + public function add_event(\core\event\base $event) { + /* Number events from 0. */ + $this->events[] = $event; + } + + /** + * Returns all redirected events. + * + * The instances are records form the event_read table. + * The array indexes are numbered from 0 and the order is matching + * the creation of events. + * + * @return \core\event\base[] + */ + public function get_events() { + return $this->events; + } + + /** + * Return number of events redirected to this sink. + * + * @return int + */ + public function count() { + return count($this->events); + } + + /** + * Removes all previously stored events. + */ + public function clear() { + $this->events = array(); + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(event_sink::class, \phpunit_event_sink::class); diff --git a/public/lib/classes/test/phpunit/message_sink.php b/public/lib/classes/test/phpunit/message_sink.php new file mode 100644 index 00000000000..f4ad1b47266 --- /dev/null +++ b/public/lib/classes/test/phpunit/message_sink.php @@ -0,0 +1,116 @@ +. + +namespace core\test\phpunit; + +/** + * Message sink. + * + * @package core + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class message_sink { + /** @var array of records from messages table */ + protected $messages = array(); + + /** + * Stop message redirection. + * + * Use if you do not want message redirected any more. + */ + public function close() { + phpunit_util::stop_message_redirection(); + } + + /** + * To be called from phpunit_util only! + * + * @param \stdClass $message record from messages table + */ + public function add_message($message) { + /* Number messages from 0. */ + $this->messages[] = $message; + } + + /** + * Returns all redirected messages. + * + * The instances are records from the messages table. + * The array indexes are numbered from 0 and the order is matching + * the creation of events. + * + * @param callable|null $filter Use to filter the messages. + * @return array + */ + public function get_messages(?callable $filter = null): array { + if ($filter) { + return array_filter($this->messages, $filter); + } + return $this->messages; + } + + /** + * Return all redirected messages for a given component. + * + * @param string $component Component name. + * @return array List of messages. + */ + public function get_messages_by_component(string $component): array { + $component = \core\component::normalize_componentname($component); + + return $this->get_messages( + fn ($message) => \core\component::normalize_componentname($message->component) === $component, + ); + } + + /** + * Return all redirected messages for a given component and type. + * + * @param string $component Component name. + * @param string $type Message type. + * @return array List of messages. + */ + public function get_messages_by_component_and_type( + string $component, + string $type, + ): array { + return array_filter($this->get_messages_by_component($component), function($message) use ($type) { + return $message->eventtype == $type; + }); + } + + /** + * Return number of messages redirected to this sink. + * @return int + */ + public function count() { + return count($this->messages); + } + + /** + * Removes all previously stored messages. + */ + public function clear() { + $this->messages = array(); + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(message_sink::class, \phpunit_message_sink::class); diff --git a/public/lib/classes/test/phpunit/phpmailer_sink.php b/public/lib/classes/test/phpunit/phpmailer_sink.php new file mode 100644 index 00000000000..478dbaa2292 --- /dev/null +++ b/public/lib/classes/test/phpunit/phpmailer_sink.php @@ -0,0 +1,84 @@ +. + +namespace core\test\phpunit; + +/** + * Message sink for phpmailer. + * + * @package core + * @category test + * @copyright Andrew Nicols . + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class phpmailer_sink { + /** + * @var array of records which would have been sent by phpmailer. + */ + protected $messages = array(); + + /** + * Stop message redirection. + * + * Use if you do not want message redirected any more. + */ + public function close() { + phpunit_util::stop_phpmailer_redirection(); + } + + /** + * To be called from phpunit_util only! + * + * @param stdClass $message record from messages table + */ + public function add_message($message) { + /* Number messages from 0. */ + $this->messages[] = $message; + } + + /** + * Returns all redirected messages. + * + * The instances are records from the messages table. + * The array indexes are numbered from 0 and the order is matching + * the creation of events. + * + * @return array + */ + public function get_messages() { + return $this->messages; + } + + /** + * Return number of messages redirected to this sink. + * @return int + */ + public function count() { + return count($this->messages); + } + + /** + * Removes all previously stored messages. + */ + public function clear() { + $this->messages = array(); + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(phpmailer_sink::class, \phpunit_phpmailer_sink::class); diff --git a/public/lib/classes/test/phpunit/phpunit_util.php b/public/lib/classes/test/phpunit/phpunit_util.php new file mode 100644 index 00000000000..5a34ce16cc8 --- /dev/null +++ b/public/lib/classes/test/phpunit/phpunit_util.php @@ -0,0 +1,1080 @@ +. + +namespace core\test\phpunit; + +// phpcs:disable moodle.Commenting.ValidTags.Invalid +// phpcs:disable moodle.PHP.ForbiddenFunctions.FoundWithAlternative +// phpcs:disable moodle.Files.MoodleInternal.MoodleInternalGlobalState + +use stdClass; + +/** + * A collection of utility methods for PHPUnit. + * + * @package core + * @copyright Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class phpunit_util extends \core\test\testing_util { + /** + * @var int last value of db writes counter, used for db resetting + */ + public static $lastdbwrites = null; + + /** @var array An array of original globals, restored after each test */ + protected static $globals = []; + + /** @var array list of debugging messages triggered during the last test execution */ + protected static $debuggings = []; + + /** @var message_sink alternative target for moodle messaging */ + protected static $messagesink = null; + + /** @var phpmailer_sink alternative target for phpmailer messaging */ + protected static $phpmailersink = null; + + /** @var message_sink alternative target for moodle messaging */ + protected static $eventsink = null; + + /** + * @var array Files to skip when resetting dataroot folder + */ + protected static $datarootskiponreset = ['.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess']; + + /** + * @var array Files to skip when dropping dataroot folder + */ + protected static $datarootskipondrop = ['.', '..', 'lock']; + + /** + * Load global $CFG; + * + * @internal + */ + public static function initialise_cfg() { + global $DB; + + $dbhash = false; + try { + $dbhash = $DB->get_field('config', 'value', ['name' => 'phpunittest']); + } catch (\Exception $e) { + // Not installed yet. + initialise_cfg(); + return; + } + if ($dbhash !== \core\component::get_all_versions_hash()) { + // Do not set CFG - the only way forward is to drop and reinstall. + return; + } + // Standard CFG init. + initialise_cfg(); + } + + /** + * Reset contents of all database tables to initial values, reset caches, etc. + * + * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care! + * + * @param bool $detectchanges + * true - changes in global state and database are reported as errors + * false - no errors reported + * null - only critical problems are reported as errors + */ + public static function reset_all_data($detectchanges = false) { + global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $FULLME, $FILTERLIB_PRIVATE; + + // Stop all hook redirections. + \core\di::get(\core\hook\manager::class)->phpunit_stop_redirections(); + + // Stop any message redirection. + self::stop_message_redirection(); + + // Stop any message redirection. + self::stop_event_redirection(); + + // Start a new email redirection. + // This will clear any existing phpmailer redirection. + // We redirect all phpmailer output to this message sink which is + // called instead of phpmailer actually sending the message. + self::start_phpmailer_redirection(); + + // We used to call gc_collect_cycles here to ensure desctructors were called between tests. + // This accounted for 25% of the total time running phpunit - so we removed it. + + // Show any unhandled debugging messages, the runbare() could already reset it. + self::display_debugging_messages(); + self::reset_debugging(); + + // Reset global $DB in case somebody mocked it. + $DB = self::get_global_backup('DB'); + + if ($DB->is_transaction_started()) { + // We can not reset inside transaction. + $DB->force_transaction_rollback(); + } + + $resetdb = self::reset_database(); + $localename = self::get_locale_name(); + $warnings = []; + + if ($detectchanges === true) { + if ($resetdb) { + $warnings[] = 'Warning: unexpected database modification, resetting DB state'; + } + + $oldcfg = self::get_global_backup('CFG'); + $oldsite = self::get_global_backup('SITE'); + foreach ($CFG as $k => $v) { + if (!property_exists($oldcfg, $k)) { + $warnings[] = 'Warning: unexpected new $CFG->' . $k . ' value'; + } else if ($oldcfg->$k !== $CFG->$k) { + $warnings[] = 'Warning: unexpected change of $CFG->' . $k . ' value'; + } + unset($oldcfg->$k); + } + if ($oldcfg) { + foreach ($oldcfg as $k => $v) { + $warnings[] = 'Warning: unexpected removal of $CFG->' . $k; + } + } + + if ($USER->id != 0) { + $warnings[] = 'Warning: unexpected change of $USER'; + } + + if ($COURSE->id != $oldsite->id) { + $warnings[] = 'Warning: unexpected change of $COURSE'; + } + + if ($FULLME !== self::get_global_backup('FULLME')) { + $warnings[] = 'Warning: unexpected change of $FULLME'; + } + + if (setlocale(LC_TIME, 0) !== $localename) { + $warnings[] = 'Warning: unexpected change of locale'; + } + } + + if (ini_get('max_execution_time') != 0) { + // This is special warning for all resets because we do not want any + // libraries to mess with timeouts unintentionally. + // Our PHPUnit integration is not supposed to change it either. + + if ($detectchanges !== false) { + $warnings[] = 'Warning: max_execution_time was changed to ' . ini_get('max_execution_time'); + } + set_time_limit(0); + } + + // Restore original globals. + $_SERVER = self::get_global_backup('_SERVER'); + $CFG = self::get_global_backup('CFG'); + $SITE = self::get_global_backup('SITE'); + $FULLME = self::get_global_backup('FULLME'); + $_GET = []; + $_POST = []; + $_FILES = []; + $_REQUEST = []; + $COURSE = $SITE; + + // Reinitialise following globals. + $OUTPUT = new \bootstrap_renderer(); + $PAGE = new \moodle_page(); + \navigation_node::reset_all_data(); + $FULLME = null; + $ME = null; + $SCRIPT = null; + $FILTERLIB_PRIVATE = null; + if (!empty($SESSION->notifications)) { + $SESSION->notifications = []; + } + + // Empty sessison and set fresh new not-logged-in user. + \core\session\manager::init_empty_session(); + + // Reset all static caches. + \core\event\manager::phpunit_reset(); + accesslib_clear_all_caches(true); + accesslib_reset_role_cache(); + get_string_manager()->reset_caches(true); + reset_text_filters_cache(true); + get_message_processors(false, true, true); + \filter_manager::reset_caches(); + \core_filetypes::reset_caches(); + \core_search\manager::clear_static(); + \core\user::reset_caches(); + \core\output\icon_system::reset_caches(); + if (class_exists(\core_media_manager::class, false)) { + \core_media_manager::reset_caches(); + } + + // Reset static unit test options. + if (class_exists(\availability_date\condition::class, false)) { + \availability_date\condition::set_current_time_for_test(0); + } + + // Reset internal users. + \core\user::reset_internal_users(); + + // Clear static caches in calendar container. + if (class_exists(\core_calendar\local\event\container::class, false)) { + \core_calendar\local\event\container::reset_caches(); + } + + // Reset hub registration caches. + \core\hub\registration::reset_caches(); + + // TODO MDL-25290: add more resets here and probably refactor them to new core function. + + // Reset course and module caches. + \core_courseformat\base::reset_course_cache(0); + get_fast_modinfo(0, 0, true); + + // Reset other singletons. + if (class_exists(\core_plugin_manager::class)) { + \core_plugin_manager::reset_caches(true); + } + if (class_exists(\core\update\checker::class)) { + \core\update\checker::reset_caches(true); + } + if (class_exists(\core_course\customfield\course_handler::class)) { + \core_course\customfield\course_handler::reset_caches(); + } + if (class_exists(\core_reportbuilder\manager::class)) { + \core_reportbuilder\manager::reset_caches(); + } + if (class_exists(\core_cohort\customfield\cohort_handler::class)) { + \core_cohort\customfield\cohort_handler::reset_caches(); + } + if (class_exists(\core_group\customfield\group_handler::class)) { + \core_group\customfield\group_handler::reset_caches(); + } + if (class_exists(\core_group\customfield\grouping_handler::class)) { + \core_group\customfield\grouping_handler::reset_caches(); + } + if (class_exists(\core_reportbuilder\customfield\report_handler::class)) { + \core_reportbuilder\customfield\report_handler::reset_caches(); + } + if (class_exists(\core_customfield\customfield\shared_handler::class)) { + \core_customfield\customfield\shared_handler::reset_caches(); + } + + // Clear static cache within restore. + if (class_exists(\restore_section_structure_step::class)) { + \restore_section_structure_step::reset_caches(); + } + + // Purge dataroot directory. + self::reset_dataroot(); + + // Restore original config once more in case resetting of caches changed CFG. + $CFG = self::get_global_backup('CFG'); + + // Inform data generator. + self::get_data_generator()->reset(); + + // Fix PHP settings. + error_reporting($CFG->debug); + + // Reset the date/time class. + \core_date::phpunit_reset(); + + // Make sure the time locale is consistent - that is Australian English. + setlocale(LC_TIME, $localename); + + // Reset the log manager cache. + get_log_manager(true); + + // Reset user agent. + \core_useragent::instance(true, null); + + // Reset the DI container. + \core\di::reset_container(); + + // Verify db writes just in case something goes wrong in reset. + if (self::$lastdbwrites != $DB->perf_get_writes()) { + error_log('Unexpected DB writes in phpunit_util::reset_all_data()'); + self::$lastdbwrites = $DB->perf_get_writes(); + } + + if ($warnings) { + $warnings = implode("\n", $warnings); + throw new \core_phpunit\exception\test_exception($warnings); + } + } + + /** + * Reset all database tables to default values. + * + * @return bool true if reset done, false if skipped + */ + public static function reset_database() { + global $DB; + + if (defined('PHPUNIT_ISOLATED_TEST') && PHPUNIT_ISOLATED_TEST && self::$lastdbwrites === null) { + // This is an isolated test and the lastdbwrites has not yet been initialised. + // Isolated test runs are reset by the test runner before the run starts. + self::$lastdbwrites = $DB->perf_get_writes(); + } + + if (!is_null(self::$lastdbwrites) && self::$lastdbwrites == $DB->perf_get_writes()) { + return false; + } + + if (!parent::reset_database()) { + return false; + } + + self::$lastdbwrites = $DB->perf_get_writes(); + + return true; + } + + /** + * Called during bootstrap only! + * @internal + * + * @return void + */ + public static function bootstrap_init() { + global $CFG, $SITE, $DB, $FULLME; + + // Backup the globals. + self::$globals['_SERVER'] = $_SERVER; + self::$globals['CFG'] = clone($CFG); + self::$globals['SITE'] = clone($SITE); + self::$globals['DB'] = $DB; + self::$globals['FULLME'] = $FULLME; + + // Refresh data in all tables, clear caches, etc. + self::reset_all_data(); + } + + /** + * Print some Moodle related info to console. + * + * @internal + */ + public static function bootstrap_moodle_info() { + echo self::get_site_info(); + } + + /** + * Returns original state of global variable. + * + * @param string $name + * @return mixed + */ + public static function get_global_backup($name) { + if ($name === 'DB') { + // No cloning of database object, + // We just need the original reference, not original state. + return self::$globals['DB']; + } + if (isset(self::$globals[$name])) { + if (is_object(self::$globals[$name])) { + $return = clone(self::$globals[$name]); + return $return; + } else { + return self::$globals[$name]; + } + } + return null; + } + + /** + * Is this site initialised to run unit tests? + * + * @return array errorcode=>message, 0 means ok + */ + public static function testing_ready_problem() { + global $DB; + + $localename = self::get_locale_name(); + if (setlocale(LC_TIME, $localename) === false) { + return [PHPUNIT_EXITCODE_CONFIGERROR, "Required locale '$localename' is not installed."]; + } + + if (!self::is_test_site()) { + // Dataroot was verified in bootstrap, so it must be DB. + return [PHPUNIT_EXITCODE_CONFIGERROR, 'Can not use database for testing, try different prefix']; + } + + $tables = $DB->get_tables(false); + if (empty($tables)) { + return [PHPUNIT_EXITCODE_INSTALL, '']; + } + + if (!self::is_test_data_updated()) { + return [PHPUNIT_EXITCODE_REINSTALL, '']; + } + + return [0, '']; + } + + /** + * Drop all test site data. + * + * Note: To be used from CLI scripts only. + * + * @param bool $displayprogress if true, this method will echo progress information. + * @return void may terminate execution with exit code + */ + public static function drop_site($displayprogress = false) { + global $DB, $CFG; + + if (!self::is_test_site()) { + phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!'); + } + + // Purge dataroot. + if ($displayprogress) { + echo "Purging dataroot:\n"; + } + + self::reset_dataroot(); + testing_initdataroot($CFG->dataroot, 'phpunit'); + + // Drop all tables. + self::drop_database($displayprogress); + + // Drop dataroot. + self::drop_dataroot(); + } + + /** + * Perform a fresh test site installation + * + * Note: To be used from CLI scripts only. + * + * @return void may terminate execution with exit code + */ + public static function install_site() { + global $DB, $CFG; + + if (!self::is_test_site()) { + phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not install on non-test site!!'); + } + + if ($DB->get_tables()) { + [$errorcode, $message] = self::testing_ready_problem(); + if ($errorcode) { + phpunit_bootstrap_error( + PHPUNIT_EXITCODE_REINSTALL, + 'Database tables already present, Moodle PHPUnit test environment can not be initialised', + ); + } else { + phpunit_bootstrap_error(0, 'Moodle PHPUnit test environment is already initialised'); + } + } + + $options = []; + $options['adminpass'] = 'admin'; + $options['shortname'] = 'phpunit'; + $options['fullname'] = 'PHPUnit test site'; + + install_cli_database($options, false); + + // Set the admin email address. + $DB->set_field('user', 'email', 'admin@example.com', ['username' => 'admin']); + + // Disable all logging for performance and sanity reasons. + set_config('enabled_stores', '', 'tool_log'); + + // Remove any default blocked hosts and port restrictions, to avoid blocking tests (eg those using local files). + set_config('curlsecurityblockedhosts', ''); + set_config('curlsecurityallowedport', ''); + + // Execute all the adhoc tasks. + while ($task = \core\task\manager::get_next_adhoc_task(time())) { + $task->execute(); + \core\task\manager::adhoc_task_complete($task); + } + + // We need to keep the installed dataroot filedir files. + // So each time we reset the dataroot before running a test, the default files are still installed. + self::save_original_data_files(); + + // Store version hash in the database and in a file. + self::store_versions_hash(); + + // Store database data and structure. + self::store_database_state(); + } + + /** + * Builds root/phpunit.xml file using defaults from /phpunit.xml.dist + * + * @return bool true means main config file created, false means only dataroot file created + */ + public static function build_config_file() { + global $CFG; + + $template = << + public/@dir@ + public/@dir@/classes + public/@dir@/fixtures + + + EOF; + $data = file_get_contents("$CFG->root/phpunit.xml.dist"); + + $suites = ''; + $includelists = []; + $excludelists = []; + + $subsystems = \core\component::get_core_subsystems(); + $subsystems['core'] = $CFG->dirroot . '/lib'; + foreach ($subsystems as $subsystem => $fulldir) { + if (empty($fulldir)) { + continue; + } + if (!file_exists("{$fulldir}/tests/")) { + // There are no tests - skip this directory. + continue; + } + + $dir = substr($fulldir, strlen($CFG->dirroot) + 1); + if ($coverageinfo = self::get_coverage_info($fulldir)) { + $includelists = array_merge($includelists, $coverageinfo->get_includelists("public/{$dir}")); + $excludelists = array_merge($excludelists, $coverageinfo->get_excludelists("public/{$dir}")); + } + } + + $plugintypes = \core\component::get_plugin_types(); + ksort($plugintypes); + foreach (array_keys($plugintypes) as $type) { + $plugs = \core\component::get_plugin_list($type); + ksort($plugs); + foreach ($plugs as $plug => $plugindir) { + if (!file_exists("{$plugindir}/tests/")) { + // There are no tests - skip this directory. + continue; + } + + $dir = substr($plugindir, strlen($CFG->dirroot) + 1); + $testdir = "{$dir}/tests"; + $component = "{$type}_{$plug}"; + + $suite = str_replace('@component@', $component, $template); + $suite = str_replace('@dir@', $testdir, $suite); + + $suites .= $suite; + + if ($coverageinfo = self::get_coverage_info($plugindir)) { + $includelists = array_merge($includelists, $coverageinfo->get_includelists("public/{$dir}")); + $excludelists = array_merge($excludelists, $coverageinfo->get_excludelists("public/{$dir}")); + } + } + } + + // Start a sequence between 100000 and 199000 to ensure each call to init produces + // different ids in the database. This reduces the risk that hard coded values will + // end up being placed in phpunit or behat test code. + $sequencestart = 100000 + mt_rand(0, 99) * 1000; + + $data = preg_replace('| *.*|s', trim($suites, "\n"), $data, 1); + $data = str_replace( + '', + '', + $data + ); + + $coverages = self::get_coverage_config($includelists, $excludelists); + $data = preg_replace('| *|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"); + } + } + + return (bool)$result; + } + + /** + * Builds phpunit.xml files for all components using defaults from /phpunit.xml.dist + * + * @return void, stops if can not write files + */ + public static function build_component_config_files() { + global $CFG; + + $template = << + + tests + tests/classes + tests/fixtures + + + EOT; + $coveragedefault = << + . + + + tests + + EOT; + + // Start a sequence between 100000 and 199000 to ensure each call to init produces + // different ids in the database. This reduces the risk that hard coded values will + // end up being placed in phpunit or behat test code. + $sequencestart = 100000 + mt_rand(0, 99) * 1000; + + // Use the upstream file as source for the distributed configurations. + $ftemplate = file_get_contents("$CFG->root/phpunit.xml.dist"); + $ftemplate = preg_replace('| *', $ftemplate); + + // Gets all the components with tests. + $components = \tests_finder::get_components_with_tests('phpunit'); + + // Create the corresponding phpunit.xml file for each component. + foreach ($components as $cname => $cpath) { + // Calculate the component suite. + $ctemplate = $template; + $ctemplate = str_replace('@component@', $cname, $ctemplate); + + $fcontents = str_replace('', $ctemplate, $ftemplate); + + // Check for coverage configurations. + if ($coverageinfo = self::get_coverage_info($cpath)) { + $coverages = self::get_coverage_config($coverageinfo->get_includelists(''), $coverageinfo->get_excludelists('')); + } else { + $coverages = $coveragedefault; + } + $fcontents = preg_replace('| *|s', trim($coverages, "\n"), $fcontents); + + // Apply it to the file template. + $fcontents = str_replace( + '', + '', + $fcontents + ); + + // Fix link to schema. + $level = substr_count(str_replace('\\', '/', $cpath), '/') - substr_count(str_replace('\\', '/', $CFG->dirroot), '/'); + $fcontents = str_replace('public/lib/phpunit/', str_repeat('../', $level) . 'lib/phpunit/', $fcontents); + + // Write the file. + $result = false; + if (is_writable($cpath)) { + if ($result = (bool)file_put_contents("$cpath/phpunit.xml", $fcontents)) { + testing_fix_file_permissions("$cpath/phpunit.xml"); + } + } + // Problems writing file, throw error. + if (!$result) { + phpunit_bootstrap_error( + PHPUNIT_EXITCODE_CONFIGWARNING, + "Can not create $cpath/phpunit.xml configuration file, verify dir permissions", + ); + } + } + } + + /** + * To be called from debugging() only. + * @param string $message + * @param int $level + * @param string $from + */ + public static function debugging_triggered($message, $level, $from) { + // Store only if debugging triggered from actual test, + // we need normal debugging outside of tests to find problems in our phpunit integration. + $backtrace = debug_backtrace(); + + // Only for advanced_testcase, database_driver_testcase (and descendants). Others aren't + // able to manage the debugging sink, so any debugging has to be output normally and, hopefully, + // PHPUnit execution will catch that unexpected output properly. + $sinksupport = false; + foreach ($backtrace as $bt) { + if ( + isset($bt['object']) + && is_object($bt['object']) + && ( + $bt['object'] instanceof \advanced_testcase + || $bt['object'] instanceof \database_driver_testcase) + ) { + $sinksupport = true; + break; + } + } + if (!$sinksupport) { + return false; + } + + // Verify that we are inside a PHPUnit test (little bit redundant, because + // we already have checked above that this is an advanced/database_driver + // testcase, but let's keep things double safe for now). + foreach ($backtrace as $bt) { + if ( + isset($bt['object']) && is_object($bt['object']) + && $bt['object'] instanceof \PHPUnit\Framework\TestCase + ) { + $debug = new stdClass(); + $debug->message = $message; + $debug->level = $level; + $debug->from = $from; + + self::$debuggings[] = $debug; + + return true; + } + } + return false; + } + + /** + * Resets the list of debugging messages. + */ + public static function reset_debugging() { + self::$debuggings = []; + set_debugging(DEBUG_DEVELOPER); + } + + /** + * Returns all debugging messages triggered during test. + * @return array with instances having message, level and stacktrace property. + */ + public static function get_debugging_messages() { + return self::$debuggings; + } + + /** + * Prints out any debug messages accumulated during test execution. + * + * @param bool $return true to return the messages or false to print them directly. Default false. + * @return bool|string false if no debug messages, true if debug triggered or string of messages + */ + public static function display_debugging_messages($return = false) { + if (empty(self::$debuggings)) { + return false; + } + + $debugstring = ''; + foreach (self::$debuggings as $debug) { + $debugstring .= 'Debugging: ' . $debug->message . "\n" . trim($debug->from) . "\n"; + } + + if ($return) { + return $debugstring; + } + echo $debugstring; + return true; + } + + /** + * Start message redirection. + * + * Note: Do not call directly from tests, + * use $sink = $this->redirectMessages() instead. + * + * @return message_sink + */ + public static function start_message_redirection() { + if (self::$messagesink) { + self::stop_message_redirection(); + } + self::$messagesink = new message_sink(); + return self::$messagesink; + } + + /** + * End message redirection. + * + * Note: Do not call directly from tests, + * use $sink->close() instead. + */ + public static function stop_message_redirection() { + self::$messagesink = null; + } + + /** + * Are messages redirected to some sink? + * + * Note: to be called from messagelib.php only! + * + * @return bool + */ + public static function is_redirecting_messages() { + return !empty(self::$messagesink); + } + + /** + * To be called from messagelib.php only! + * + * @param stdClass $message record from messages table + * @return bool true means send message, false means message "sent" to sink. + */ + public static function message_sent($message) { + if (self::$messagesink) { + self::$messagesink->add_message($message); + } + } + + /** + * Start phpmailer redirection. + * + * Note: Do not call directly from tests, + * use $sink = $this->redirectEmails() instead. + * + * @return phpmailer_sink + */ + public static function start_phpmailer_redirection() { + if (self::$phpmailersink) { + // If an existing mailer sink is active, just clear it. + self::$phpmailersink->clear(); + } else { + self::$phpmailersink = new phpmailer_sink(); + } + return self::$phpmailersink; + } + + /** + * End phpmailer redirection. + * + * Note: Do not call directly from tests, + * use $sink->close() instead. + */ + public static function stop_phpmailer_redirection() { + self::$phpmailersink = null; + } + + /** + * Are messages for phpmailer redirected to some sink? + * + * Note: to be called from moodle_phpmailer.php only! + * + * @return bool + */ + public static function is_redirecting_phpmailer() { + return !empty(self::$phpmailersink); + } + + /** + * To be called from messagelib.php only! + * + * @param stdClass $message record from messages table + * @return bool true means send message, false means message "sent" to sink. + */ + public static function phpmailer_sent($message) { + if (self::$phpmailersink) { + self::$phpmailersink->add_message($message); + } + } + + /** + * Start event redirection. + * + * @private + * Note: Do not call directly from tests, + * use $sink = $this->redirectEvents() instead. + * + * @return event_sink + */ + public static function start_event_redirection() { + if (self::$eventsink) { + self::stop_event_redirection(); + } + self::$eventsink = new event_sink(); + return self::$eventsink; + } + + /** + * End event redirection. + * + * @private + * Note: Do not call directly from tests, + * use $sink->close() instead. + */ + public static function stop_event_redirection() { + self::$eventsink = null; + } + + /** + * Are events redirected to some sink? + * + * Note: to be called from \core\event\base only! + * + * @private + * @return bool + */ + public static function is_redirecting_events() { + return !empty(self::$eventsink); + } + + /** + * To be called from \core\event\base only! + * + * @private + * @param \core\event\base $event record from event_read table + * @return bool true means send event, false means event "sent" to sink. + */ + public static function event_triggered(\core\event\base $event) { + if (self::$eventsink) { + self::$eventsink->add_event($event); + } + } + + /** + * Gets the name of the locale for testing environment (Australian English) + * depending on platform environment. + * + * @return string the locale name. + */ + protected static function get_locale_name() { + global $CFG; + if ($CFG->ostype === 'WINDOWS') { + return 'English_Australia.1252'; + } else { + return 'en_AU.UTF-8'; + } + } + + /** + * Executes all adhoc tasks in the queue. Useful for testing asynchronous behaviour. + * + * @return void + */ + public static function run_all_adhoc_tasks() { + $now = time(); + while (($task = \core\task\manager::get_next_adhoc_task($now)) !== null) { + try { + $task->execute(); + \core\task\manager::adhoc_task_complete($task); + } catch (\Exception $e) { + \core\task\manager::adhoc_task_failed($task); + } + } + } + + /** + * Helper function to call a protected/private method of an object using reflection. + * + * Example 1. Calling a protected object method: + * $result = call_internal_method($myobject, 'method_name', [$param1, $param2], '\my\namespace\myobjectclassname'); + * + * Example 2. Calling a protected static method: + * $result = call_internal_method(null, 'method_name', [$param1, $param2], '\my\namespace\myclassname'); + * + * @param object|null $object the object on which to call the method, or null if calling a static method. + * @param string $methodname the name of the protected/private method. + * @param array $params the array of function params to pass to the method. + * @param string $classname the fully namespaced name of the class the object was created from (base in the case of mocks), + * or the name of the static class when calling a static method. + * @return mixed the respective return value of the method. + */ + public static function call_internal_method($object, $methodname, array $params, $classname) { + $reflection = new \ReflectionClass($classname); + $method = $reflection->getMethod($methodname); + return $method->invokeArgs($object, $params); + } + + /** + * Pad the supplied string with $level levels of indentation. + * + * @param string $string The string to pad + * @param int $level The number of levels of indentation to pad + * @return string + */ + protected static function pad(string $string, int $level): string { + return str_repeat(" ", $level * 2) . "{$string}\n"; + } + + /** + * Normalise any text to always use unix line endings (line-feeds). + * + * @param string $text The text to normalize + * @return string + */ + public static function normalise_line_endings(string $text): string { + return str_replace(["\r\n", "\r"], "\n", $text); + } + + /** + * Get the coverage config for the supplied includelist and excludelist configuration. + * + * @param string[] $includelists The list of files/folders in the includelist. + * @param string[] $excludelists The list of files/folders in the excludelist. + * @return string + */ + protected static function get_coverage_config(array $includelists, array $excludelists): string { + $coverages = ''; + if (!empty($includelists)) { + $coverages .= self::pad("", 2); + foreach ($includelists as $line) { + $coverages .= self::pad($line, 3); + } + $coverages .= self::pad("", 2); + if (!empty($excludelists)) { + $coverages .= self::pad("", 2); + foreach ($excludelists as $line) { + $coverages .= self::pad($line, 3); + } + $coverages .= self::pad("", 2); + } + } + + return $coverages; + } + + /** + * 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 + */ + protected static function get_coverage_info(string $fulldir): \core\test\phpunit\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"); + } + + return $coverageinfo; + } + + return new \core\test\phpunit\coverage_info(); + } + + /** + * Whether the current process is an isolated test process. + * + * @return bool + */ + public static function is_in_isolated_process(): bool { + // Note: There is no function to call, or much to go by in order to tell whether we are in an isolated process + // during Bootstrap, when this function is called. + // We can do so by testing the existence of the wrapper function, but there is nothing set until that point. + return function_exists('__phpunit_run_isolated_test'); + } + + #[\Override] + protected static function get_framework() { + return 'phpunit'; + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(phpunit_util::class, \phpunit_util::class); diff --git a/public/lib/classes/test/test_finder.php b/public/lib/classes/test/test_finder.php new file mode 100644 index 00000000000..cec2a1e80da --- /dev/null +++ b/public/lib/classes/test/test_finder.php @@ -0,0 +1,204 @@ +. + +namespace core\test; + +/** + * Finds components and plugins with tests + * + * @package core + * @category test + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class test_finder { + /** + * Returns all the components with tests of the specified type + * @param string $testtype The kind of test we are looking for + * @return array + */ + public static function get_components_with_tests($testtype) { + + // Get all the components + $components = self::get_all_plugins_with_tests($testtype) + self::get_all_subsystems_with_tests($testtype); + + // Get all the directories having tests + $directories = self::get_all_directories_with_tests($testtype); + + // Find any directory not covered by proper components + $remaining = array_diff($directories, $components); + + // Add them to the list of components + $components += $remaining; + + return $components; + } + + /** + * Returns all the plugins having tests + * @param string $testtype The kind of test we are looking for + * @return array all the plugins having tests + */ + private static function get_all_plugins_with_tests($testtype) { + $pluginswithtests = array(); + + $plugintypes = \core\component::get_plugin_types(); + ksort($plugintypes); + foreach ($plugintypes as $type => $unused) { + $plugs = \core\component::get_plugin_list($type); + ksort($plugs); + foreach ($plugs as $plug => $fullplug) { + // Look for tests recursively + if (self::directory_has_tests($fullplug, $testtype)) { + $pluginswithtests[$type . '_' . $plug] = $fullplug; + } + } + } + return $pluginswithtests; + } + + /** + * Returns all the subsystems having tests + * + * Note we are hacking here the list of subsystems + * to cover some well-known subsystems that are not properly + * returned by the {@link get_core_subsystems()} function. + * + * @param string $testtype The kind of test we are looking for + * @return array all the subsystems having tests + */ + private static function get_all_subsystems_with_tests($testtype) { + global $CFG; + + $subsystemswithtests = array(); + + $subsystems = \core\component::get_core_subsystems(); + + // Hack the list a bit to cover some well-known ones + $subsystems['backup'] = $CFG->dirroot.'/backup'; + $subsystems['db-dml'] = $CFG->dirroot.'/lib/dml'; + $subsystems['db-ddl'] = $CFG->dirroot.'/lib/ddl'; + + ksort($subsystems); + foreach ($subsystems as $subsys => $fullsubsys) { + if ($fullsubsys === null) { + continue; + } + if (!is_dir($fullsubsys)) { + continue; + } + // Look for tests recursively + if (self::directory_has_tests($fullsubsys, $testtype)) { + $subsystemswithtests['core_' . $subsys] = $fullsubsys; + } + } + return $subsystemswithtests; + } + + /** + * Returns all the directories having tests + * + * @param string $testtype The kind of test we are looking for + * @return array all directories having tests + */ + private static function get_all_directories_with_tests($testtype) { + global $CFG; + + // List of directories to exclude from test file searching. + $excludedir = array('node_modules', 'vendor'); + + // Get first level directories in which tests should be searched. + $directoriestosearch = array(); + $alldirs = glob($CFG->dirroot . DIRECTORY_SEPARATOR . '*' , GLOB_ONLYDIR); + foreach ($alldirs as $dir) { + if (!in_array(basename($dir), $excludedir) && (filetype($dir) != 'link')) { + $directoriestosearch[] = $dir; + } + } + + // Search for tests in valid directories. + $dirs = array(); + foreach ($directoriestosearch as $dir) { + $dirite = new \RecursiveDirectoryIterator($dir); + $iteite = new \RecursiveIteratorIterator($dirite); + $regexp = self::get_regexp($testtype); + $regite = new \RegexIterator($iteite, $regexp); + foreach ($regite as $path => $element) { + $key = dirname(dirname($path)); + $value = trim(str_replace(DIRECTORY_SEPARATOR, '_', str_replace($CFG->dirroot, '', $key)), '_'); + $dirs[$key] = $value; + } + } + ksort($dirs); + return array_flip($dirs); + } + + /** + * Returns if a given directory has tests (recursively) + * + * @param string $dir full path to the directory to look for phpunit tests + * @param string $testtype phpunit|behat + * @return bool if a given directory has tests (true) or no (false) + */ + private static function directory_has_tests($dir, $testtype) { + if (!is_dir($dir)) { + return false; + } + + $dirite = new \RecursiveDirectoryIterator($dir); + $iteite = new \RecursiveIteratorIterator($dirite); + $regexp = self::get_regexp($testtype); + $regite = new \RegexIterator($iteite, $regexp); + $regite->rewind(); + if ($regite->valid()) { + return true; + } + return false; + } + + + /** + * Returns the regular expression to match by the test files + * @param string $testtype + * @return string + */ + private static function get_regexp($testtype) { + + $sep = preg_quote(DIRECTORY_SEPARATOR, '|'); + + switch ($testtype) { + case 'phpunit': + $regexp = '|'.$sep.'tests'.$sep.'.*_test\.php$|'; + break; + case 'features': + $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'.*\.feature$|'; + break; + case 'stepsdefinitions': + $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'behat_.*\.php$|'; + break; + case 'behat': + $regexp = '!'.$sep.'tests'.$sep.'behat'.$sep.'(.*\.feature)|(behat_.*\.php)$!'; + break; + } + + return $regexp; + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(test_finder::class, \tests_finder::class); diff --git a/public/lib/testing/classes/util.php b/public/lib/classes/test/testing_util.php similarity index 93% rename from public/lib/testing/classes/util.php rename to public/lib/classes/test/testing_util.php index 3073cde3bf7..266ab47b00f 100644 --- a/public/lib/testing/classes/util.php +++ b/public/lib/classes/test/testing_util.php @@ -14,8 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace core\test; + +use testing_data_generator; + /** - * Testing util classes + * Testing utility classes. * * @package core * @category test @@ -78,6 +82,11 @@ abstract class testing_util { */ protected static $sequencenextstartingid = null; + /** + * @var array List of dataroot files and folders to not delete during reset_dataroot. + */ + protected static $datarootskiponreset = []; + /** * Return the name of the JSON file containing the init filenames. * @@ -120,20 +129,31 @@ abstract class testing_util { * @static * @return string */ - final protected static function get_framework() { + protected static function get_framework() { + debugging('You must override testing_util::get_framework in subclass', DEBUG_DEVELOPER); + $classname = get_called_class(); + $reflectedclass = new \ReflectionClass($classname); + + if ($reflectedclass->inNamespace()) { + $namespaces = explode('\\', $reflectedclass->getNamespaceName()); + return array_shift($namespaces); + } + return substr($classname, 0, strpos($classname, '_')); } /** * Get data generator * @static - * @return testing_data_generator + * @return \testing_data_generator */ public static function get_data_generator() { + global $CFG; + if (is_null(self::$generator)) { - require_once(__DIR__ . '/../generator/lib.php'); - self::$generator = new testing_data_generator(); + require_once($CFG->libdir . '/testing/generator/lib.php'); + self::$generator = new \testing_data_generator(); } return self::$generator; } @@ -148,7 +168,7 @@ abstract class testing_util { public static function is_test_site() { global $DB, $CFG; - $framework = self::get_framework(); + $framework = static::get_framework(); if (!file_exists(self::get_dataroot() . '/' . $framework . 'testdir.txt')) { // This is already tested in bootstrap script, @@ -177,7 +197,7 @@ abstract class testing_util { public static function is_test_data_updated() { global $DB; - $framework = self::get_framework(); + $framework = static::get_framework(); $datarootpath = self::get_dataroot() . '/' . $framework; if (!file_exists($datarootpath . '/tabledata.ser') || !file_exists($datarootpath . '/tablestructure.ser')) { @@ -188,7 +208,7 @@ abstract class testing_util { return false; } - $hash = core_component::get_all_versions_hash(); + $hash = \core\component::get_all_versions_hash(); $oldhash = file_get_contents($datarootpath . '/versionshash.txt'); if ($hash !== $oldhash) { @@ -213,7 +233,7 @@ abstract class testing_util { protected static function store_database_state() { global $DB, $CFG; - $framework = self::get_framework(); + $framework = static::get_framework(); // Store data for all tables. $data = []; @@ -246,8 +266,8 @@ abstract class testing_util { protected static function store_versions_hash() { global $CFG; - $framework = self::get_framework(); - $hash = core_component::get_all_versions_hash(); + $framework = static::get_framework(); + $hash = \core\component::get_all_versions_hash(); // Add test db flag. set_config($framework . 'test', $hash); @@ -265,7 +285,7 @@ abstract class testing_util { */ protected static function get_tabledata() { if (!isset(self::$tabledata)) { - $framework = self::get_framework(); + $framework = static::get_framework(); $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser'; if (!file_exists($datafile)) { @@ -294,7 +314,7 @@ abstract class testing_util { */ public static function get_tablestructure() { if (!isset(self::$tablestructure)) { - $framework = self::get_framework(); + $framework = static::get_framework(); $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser'; if (!file_exists($structurefile)) { @@ -334,7 +354,7 @@ abstract class testing_util { self::$sequencenames = []; foreach ($structure as $table => $ignored) { - $name = $DB->get_manager()->generator->getSequenceFromDB(new xmldb_table($table)); + $name = $DB->get_manager()->generator->getSequenceFromDB(new \xmldb_table($table)); if ($name !== false) { self::$sequencenames[$table] = $name; } @@ -628,7 +648,7 @@ abstract class testing_util { // Remove extra tables. foreach ($tables as $table) { if (!isset($data[$table])) { - $DB->get_manager()->drop_table(new xmldb_table($table)); + $DB->get_manager()->drop_table(new \xmldb_table($table)); } } @@ -645,10 +665,8 @@ abstract class testing_util { public static function reset_dataroot() { global $CFG; - $childclassname = self::get_framework() . '_util'; - // Do not delete automatically installed files. - self::skip_original_data_files($childclassname); + self::skip_original_data_files(); // Clear file status cache, before checking file_exists. clearstatcache(); @@ -656,7 +674,7 @@ abstract class testing_util { // Clean up the dataroot folder. $handle = opendir(self::get_dataroot()); while (false !== ($item = readdir($handle))) { - if (in_array($item, $childclassname::$datarootskiponreset)) { + if (in_array($item, static::$datarootskiponreset)) { continue; } if (is_dir(self::get_dataroot() . "/$item")) { @@ -671,7 +689,7 @@ abstract class testing_util { if (file_exists(self::get_dataroot() . '/filedir')) { $handle = opendir(self::get_dataroot() . '/filedir'); while (false !== ($item = readdir($handle))) { - if (in_array('filedir' . DIRECTORY_SEPARATOR . $item, $childclassname::$datarootskiponreset)) { + if (in_array('filedir' . DIRECTORY_SEPARATOR . $item, static::$datarootskiponreset)) { continue; } if (is_dir(self::get_dataroot() . "/filedir/$item")) { @@ -689,11 +707,11 @@ abstract class testing_util { make_localcache_directory(''); // Purge all data from the caches. This is required for consistency between tests. // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache) - // and now we will purge any other caches as well. This must be done before the cache_factory::reset() as that + // and now we will purge any other caches as well. This must be done before the core_cache\factory::reset() as that // removes all definitions of caches and purge does not have valid caches to operate on. - cache_helper::purge_all(); + \core_cache\helper::purge_all(); // Reset the cache API so that it recreates it's required directories as well. - cache_factory::reset(); + \core_cache\factory::reset(); } /** @@ -855,7 +873,7 @@ abstract class testing_util { } $dotsonline = 0; foreach ($tables as $tablename) { - $table = new xmldb_table($tablename); + $table = new \xmldb_table($tablename); $DB->get_manager()->drop_table($table); if ($dotsonline == 60) { @@ -881,7 +899,7 @@ abstract class testing_util { protected static function drop_dataroot() { global $CFG; - $framework = self::get_framework(); + $framework = static::get_framework(); $childclassname = $framework . '_util'; $files = scandir(self::get_dataroot() . '/' . $framework); @@ -908,11 +926,8 @@ abstract class testing_util { /** * Skip the original dataroot files to not been reset. - * - * @static - * @param string $utilclassname the util class name.. */ - protected static function skip_original_data_files($utilclassname) { + protected static function skip_original_data_files() { $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson; if (file_exists($jsonfilepath)) { $listfiles = file_get_contents($jsonfilepath); @@ -922,8 +937,8 @@ abstract class testing_util { $originaldatarootfiles = json_decode($listfiles); // Keep the json file. Only drop_dataroot() should delete it. $originaldatarootfiles[] = self::$originaldatafilesjson; - $utilclassname::$datarootskiponreset = array_merge( - $utilclassname::$datarootskiponreset, + static::$datarootskiponreset = array_merge( + static::$datarootskiponreset, $originaldatarootfiles ); self::$originaldatafilesjsonadded = true; @@ -949,8 +964,8 @@ abstract class testing_util { $filedir = self::get_dataroot() . '/filedir'; if (file_exists($filedir)) { - $directory = new RecursiveDirectoryIterator($filedir); - foreach (new RecursiveIteratorIterator($directory) as $file) { + $directory = new \RecursiveDirectoryIterator($filedir); + foreach (new \RecursiveIteratorIterator($directory) as $file) { if ($file->isDir()) { $key = substr($file->getPath(), strlen(self::get_dataroot() . '/')); } else { @@ -1006,3 +1021,8 @@ abstract class testing_util { return $env; } } + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(testing_util::class, \testing_util::class); diff --git a/public/lib/db/legacyclasses.php b/public/lib/db/legacyclasses.php index 733e20ac0bd..fc81f615672 100644 --- a/public/lib/db/legacyclasses.php +++ b/public/lib/db/legacyclasses.php @@ -376,4 +376,13 @@ $legacyclasses = [ 'core_courseformat', 'output/local/content/activitychooserbutton.php', ], + + \testing_util::class => 'test/testing_util.php', + \phpunit_util::class => 'test/phpunit/phpunit_util.php', + \phpunit_coverage_info::class => 'test/phpunit/coverage_info.php', + \phpunit_message_sink::class => 'test/phpunit/message_sink.php', + \phpunit_phpmailer_sink::class => 'test/phpunit/phpmailer_sink.php', + \phpunit_event_mock::class => 'test/phpunit/event_mock.php', + \phpunit_event_sink::class => 'test/phpunit/event_sink.php', + \tests_finder::class => 'test/test_finder.php', ]; diff --git a/public/lib/dml/moodle_database.php b/public/lib/dml/moodle_database.php index aa2805c273e..b43a6511bce 100644 --- a/public/lib/dml/moodle_database.php +++ b/public/lib/dml/moodle_database.php @@ -454,7 +454,6 @@ abstract class moodle_database { defined('BEHAT_SITE_RUNNING')) { // Set list of tables that are updated. - require_once(__DIR__.'/../testing/classes/util.php'); testing_util::set_table_modified_by_sql($sql); } } diff --git a/public/lib/messagelib.php b/public/lib/messagelib.php index 8c604feb10e..8052bb06f70 100644 --- a/public/lib/messagelib.php +++ b/public/lib/messagelib.php @@ -22,8 +22,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -use core\url; - defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../message/lib.php'); @@ -356,7 +354,7 @@ function message_send(\core\message\message $eventdata) { */ function message_handle_phpunit_redirection(\core\message\message $eventdata, string $table, \stdClass $tabledata) { global $DB; - if (PHPUNIT_TEST and class_exists('phpunit_util')) { + if (PHPUNIT_TEST && class_exists(\core\test\phpunit\phpunit_util::class)) { // Add some more tests to make sure the normal code can actually work. $componentdir = core_component::get_component_directory($eventdata->component); if (!$componentdir or !is_dir($componentdir)) { @@ -374,7 +372,7 @@ function message_handle_phpunit_redirection(\core\message\message $eventdata, st unset($componentdir); unset($messageproviders); // Now ask phpunit if it wants to catch this message. - if (phpunit_util::is_redirecting_messages()) { + if (\core\test\phpunit\phpunit_util::is_redirecting_messages()) { $messageid = $DB->insert_record($table, $tabledata); $message = $DB->get_record($table, array('id' => $messageid)); @@ -402,7 +400,7 @@ function message_handle_phpunit_redirection(\core\message\message $eventdata, st // Unit tests need this detail. $message->notification = $eventdata->notification; - phpunit_util::message_sent($message); + \core\test\phpunit\phpunit_util::message_sent($message); return $messageid; } } diff --git a/public/lib/moodlelib.php b/public/lib/moodlelib.php index dbc73f6ad51..28dd11c9e33 100644 --- a/public/lib/moodlelib.php +++ b/public/lib/moodlelib.php @@ -1259,7 +1259,7 @@ function purge_caches($options = []) { * Purge all non-MUC caches not otherwise purged in purge_caches. * * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at - * {@link phpunit_util::reset_dataroot()} + * {@link \core\test\phpunit\phpunit_util::reset_dataroot()} */ function purge_other_caches() { global $DB, $CFG; diff --git a/public/lib/phpmailer/moodle_phpmailer.php b/public/lib/phpmailer/moodle_phpmailer.php index 501d1159f3f..8180131c5b2 100644 --- a/public/lib/phpmailer/moodle_phpmailer.php +++ b/public/lib/phpmailer/moodle_phpmailer.php @@ -22,8 +22,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); - // PLEASE NOTE: we use the phpmailer class _unmodified_ // through the joys of OO. Distros are free to use their stock // version of this file. @@ -129,7 +127,7 @@ class moodle_phpmailer extends \PHPMailer\PHPMailer\PHPMailer { public function postSend() { // Now ask phpunit if it wants to catch this message. if (PHPUNIT_TEST) { - if (!phpunit_util::is_redirecting_phpmailer()) { + if (!\core\test\phpunit\phpunit_util::is_redirecting_phpmailer()) { debugging('Unit tests must not send real emails! Use $this->redirectEmails()'); return true; } @@ -139,7 +137,7 @@ class moodle_phpmailer extends \PHPMailer\PHPMailer\PHPMailer { $mail->subject = $this->Subject; $mail->from = $this->From; $mail->to = $this->to[0][0]; - phpunit_util::phpmailer_sent($mail); + \core\test\phpunit\phpunit_util::phpmailer_sent($mail); return true; } else { return parent::postSend(); diff --git a/public/lib/phpunit/bootstrap.php b/public/lib/phpunit/bootstrap.php index 48dfd049174..f49b8e614e7 100644 --- a/public/lib/phpunit/bootstrap.php +++ b/public/lib/phpunit/bootstrap.php @@ -267,12 +267,12 @@ if (PHPUNIT_UTIL) { \core\hook\manager::get_instance(); // Is database and dataroot ready for testing? -[$errorcode, $message] = phpunit_util::testing_ready_problem(); +[$errorcode, $message] = \core\test\phpunit\phpunit_util::testing_ready_problem(); // Print some version info. -phpunit_util::bootstrap_moodle_info(); +\core\test\phpunit\phpunit_util::bootstrap_moodle_info(); if ($errorcode) { phpunit_bootstrap_error($errorcode, $message); } // Prepare for the first test run - store fresh globals, reset database and dataroot, etc. -phpunit_util::bootstrap_init(); +\core\test\phpunit\phpunit_util::bootstrap_init(); diff --git a/public/lib/phpunit/classes/advanced_testcase.php b/public/lib/phpunit/classes/advanced_testcase.php index d0d5a9736e1..42bf8f4fef3 100644 --- a/public/lib/phpunit/classes/advanced_testcase.php +++ b/public/lib/phpunit/classes/advanced_testcase.php @@ -17,6 +17,7 @@ use core\di; use core\hook; use core\http_client; +use core\test\phpunit\phpunit_util; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; @@ -337,7 +338,7 @@ abstract class advanced_testcase extends base_testcase { * @return void */ public function assertEventLegacyData($expected, \core\event\base $event, $message = '') { - $legacydata = phpunit_event_mock::testable_get_legacy_eventdata($event); + $legacydata = \core\test\phpunit\event_mock::testable_get_legacy_eventdata($event); if ($message === '') { $message = 'Event legacy data does not match expected value.'; } @@ -353,7 +354,7 @@ abstract class advanced_testcase extends base_testcase { * @return void */ public function assertEventLegacyLogData($expected, \core\event\base $event, $message = '') { - $legacydata = phpunit_event_mock::testable_get_legacy_logdata($event); + $legacydata = \core\test\phpunit\event_mock::testable_get_legacy_logdata($event); if ($message === '') { $message = 'Event legacy log data does not match expected value.'; } @@ -372,8 +373,8 @@ abstract class advanced_testcase extends base_testcase { */ public function assertEventContextNotUsed(\core\event\base $event, $message = '') { // Save current event->context and set it to false. - $eventcontext = phpunit_event_mock::testable_get_event_context($event); - phpunit_event_mock::testable_set_event_context($event, false); + $eventcontext = \core\test\phpunit\event_mock::testable_get_event_context($event); + \core\test\phpunit\event_mock::testable_set_event_context($event, false); if ($message === '') { $message = 'Event should not use context property of event in any method.'; } @@ -383,7 +384,7 @@ abstract class advanced_testcase extends base_testcase { $event->get_description(); // Restore event->context (note that this is unreachable when the event uses context). But ok for correct events. - phpunit_event_mock::testable_set_event_context($event, $eventcontext); + \core\test\phpunit\event_mock::testable_set_event_context($event, $eventcontext); } /** @@ -417,7 +418,7 @@ abstract class advanced_testcase extends base_testcase { * array in the returned messaging sink instance. The redirection * can be stopped by calling $sink->close(); * - * @return phpunit_message_sink + * @return \core\test\phpunit\message_sink */ public function redirectMessages() { return phpunit_util::start_message_redirection(); @@ -430,7 +431,7 @@ abstract class advanced_testcase extends base_testcase { * array in the returned phpmailer sink instance. The redirection * can be stopped by calling $sink->close(); * - * @return phpunit_message_sink + * @return \core\test\phpunit\message_sink */ public function redirectEmails() { return phpunit_util::start_phpmailer_redirection(); @@ -443,7 +444,7 @@ abstract class advanced_testcase extends base_testcase { * array in the returned event sink instance. The redirection * can be stopped by calling $sink->close(); * - * @return phpunit_event_sink + * @return \core\test\phpunit\event_sink */ public function redirectEvents() { return phpunit_util::start_event_redirection(); diff --git a/public/lib/phpunit/classes/basic_testcase.php b/public/lib/phpunit/classes/basic_testcase.php index 74fdf70e32d..92c8ea3b3d3 100644 --- a/public/lib/phpunit/classes/basic_testcase.php +++ b/public/lib/phpunit/classes/basic_testcase.php @@ -1,5 +1,4 @@ . +use PHPUnit\Framework\Attributes\After; + /** * The simplest PHPUnit test case customised for Moodle * @@ -47,11 +48,11 @@ abstract class basic_testcase extends base_testcase { global $DB; if ($DB->is_transaction_started()) { - phpunit_util::reset_all_data(); + \core\test\phpunit\phpunit_util::reset_all_data(); throw new coding_exception('basic_testcase ' . $this->getName() . ' is not supposed to use database transactions!'); } - phpunit_util::reset_all_data(true); + \core\test\phpunit\phpunit_util::reset_all_data(true); } /** diff --git a/public/lib/phpunit/classes/coverage_info.php b/public/lib/phpunit/classes/coverage_info.php index 6e0088ad0b7..daf6f9acf76 100644 --- a/public/lib/phpunit/classes/coverage_info.php +++ b/public/lib/phpunit/classes/coverage_info.php @@ -18,82 +18,14 @@ * Coverage information for PHPUnit. * * @package core - * @category phpunit - * @copyright 2018 Andrew Nicols + * @category test + * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class phpunit_coverage_info { - /** @var array The list of folders relative to the plugin root to include in coverage generation. */ - protected $includelistfolders = []; - - /** @var array The list of files relative to the plugin root to include in coverage generation. */ - protected $includelistfiles = []; - - /** @var array The list of folders relative to the plugin root to exclude from coverage generation. */ - protected $excludelistfolders = []; - - /** @var array The list of files relative to the plugin root to exclude from coverage generation. */ - protected $excludelistfiles = []; - - /** - * Get the formatted XML list of files and folders to include. - * - * @param string $plugindir The root of the plugin, relative to the dataroot. - * @return array - */ - final public function get_includelists(string $plugindir): array { - $coverages = []; - - $includelistfolders = array_merge([ - 'classes', - 'tests/generator', - ], $this->includelistfolders);; - - $includelistfiles = array_merge([ - 'externallib.php', - 'lib.php', - 'locallib.php', - 'renderer.php', - 'rsslib.php', - ], $this->includelistfiles); - - if (!empty($plugindir)) { - $plugindir .= "/"; - } - - foreach (array_unique($includelistfolders) as $folder) { - $coverages[] = html_writer::tag('directory', "{$plugindir}{$folder}", ['suffix' => '.php']); - } - - foreach (array_unique($includelistfiles) as $file) { - $coverages[] = html_writer::tag('file', "{$plugindir}{$file}"); - } - - return $coverages; - } - - /** - * Get the formatted XML list of files and folders to exclude. - * - * @param string $plugindir The root of the plugin, relative to the dataroot. - * @return array - */ - final public function get_excludelists(string $plugindir): array { - $coverages = []; - - if (!empty($plugindir)) { - $plugindir .= "/"; - } - - foreach ($this->excludelistfolders as $folder) { - $coverages[] = html_writer::tag('directory', "{$plugindir}{$folder}", ['suffix' => '.php']); - } - - foreach ($this->excludelistfiles as $file) { - $coverages[] = html_writer::tag('file', "{$plugindir}{$file}"); - } - - return $coverages; - } -} +debugging( + 'The coverage_info.php file is deprecated and will be removed in Moodle 6.0. ' . + 'It should not be manually included. ' . + 'The class can now be autoloaded from \core\test\phpunit\coverage_info.', + DEBUG_DEVELOPER +); diff --git a/public/lib/phpunit/classes/database_driver_testcase.php b/public/lib/phpunit/classes/database_driver_testcase.php index 8c18de5eef9..d745d25ec24 100644 --- a/public/lib/phpunit/classes/database_driver_testcase.php +++ b/public/lib/phpunit/classes/database_driver_testcase.php @@ -1,7 +1,5 @@ . -/** - * Event mock. - * - * @package core - * @category phpunit - * @copyright 2013 Frédéric Massart - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -require_once(__DIR__ . '/../../classes/event/base.php'); - /** * Event mock class. * @@ -33,25 +22,10 @@ require_once(__DIR__ . '/../../classes/event/base.php'); * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -abstract class phpunit_event_mock extends \core\event\base { - /** - * Returns event context. - * - * @param \core\event\base $event event to get context for. - * @return context event context - */ - public static function testable_get_event_context($event) { - return $event->context; - } - - /** - * Sets event context. - * - * @param \core\event\base $event event to set context for. - * @param context $context context to set. - */ - public static function testable_set_event_context($event, $context) { - $event->context = $context; - } -} +debugging( + 'The event_mock.php file is deprecated and will be removed in Moodle 6.0. ' . + 'It should not be manually included. ' . + 'The class can now be autoloaded from \core\test\phpunit\event_mock.', + DEBUG_DEVELOPER +); diff --git a/public/lib/phpunit/classes/event_sink.php b/public/lib/phpunit/classes/event_sink.php index df5c98c50a7..399c7e5785f 100644 --- a/public/lib/phpunit/classes/event_sink.php +++ b/public/lib/phpunit/classes/event_sink.php @@ -14,16 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Event sink. - * - * @package core - * @category phpunit - * @copyright 2013 Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - - /** * Event redirection sink. * @@ -32,56 +22,10 @@ * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class phpunit_event_sink { - /** @var \core\event\base[] array of events */ - protected $events = array(); - /** - * Stop event redirection. - * - * Use if you do not want event redirected any more. - */ - public function close() { - phpunit_util::stop_event_redirection(); - } - - /** - * To be called from phpunit_util only! - * - * @private - * @param \core\event\base $event record from event_read table - */ - public function add_event(\core\event\base $event) { - /* Number events from 0. */ - $this->events[] = $event; - } - - /** - * Returns all redirected events. - * - * The instances are records form the event_read table. - * The array indexes are numbered from 0 and the order is matching - * the creation of events. - * - * @return \core\event\base[] - */ - public function get_events() { - return $this->events; - } - - /** - * Return number of events redirected to this sink. - * - * @return int - */ - public function count() { - return count($this->events); - } - - /** - * Removes all previously stored events. - */ - public function clear() { - $this->events = array(); - } -} +debugging( + 'The event_sink.php file is deprecated and will be removed in Moodle 6.0. ' . + 'It should not be manually included. ' . + 'The class can now be autoloaded from \core\test\phpunit\event_sink.', + DEBUG_DEVELOPER +); diff --git a/public/lib/phpunit/classes/message_sink.php b/public/lib/phpunit/classes/message_sink.php index 52932a9fe98..418950e28cc 100644 --- a/public/lib/phpunit/classes/message_sink.php +++ b/public/lib/phpunit/classes/message_sink.php @@ -23,97 +23,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -/** - * Message sink. - * - * @package core - * @category phpunit - * @copyright 2012 Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class phpunit_message_sink { - /** @var array of records from messages table */ - protected $messages = array(); - - /** - * Stop message redirection. - * - * Use if you do not want message redirected any more. - */ - public function close() { - phpunit_util::stop_message_redirection(); - } - - /** - * To be called from phpunit_util only! - * - * @param stdClass $message record from messages table - */ - public function add_message($message) { - /* Number messages from 0. */ - $this->messages[] = $message; - } - - /** - * Returns all redirected messages. - * - * The instances are records from the messages table. - * The array indexes are numbered from 0 and the order is matching - * the creation of events. - * - * @param callable|null $filter Use to filter the messages. - * @return array - */ - public function get_messages(?callable $filter = null): array { - if ($filter) { - return array_filter($this->messages, $filter); - } - return $this->messages; - } - - /** - * Return all redirected messages for a given component. - * - * @param string $component Component name. - * @return array List of messages. - */ - public function get_messages_by_component(string $component): array { - $component = core_component::normalize_componentname($component); - - return $this->get_messages( - fn ($message) => core_component::normalize_componentname($message->component) === $component, - ); - } - - /** - * Return all redirected messages for a given component and type. - * - * @param string $component Component name. - * @param string $type Message type. - * @return array List of messages. - */ - public function get_messages_by_component_and_type( - string $component, - string $type, - ): array { - return array_filter($this->get_messages_by_component($component), function($message) use ($type) { - return $message->eventtype == $type; - }); - } - - /** - * Return number of messages redirected to this sink. - * @return int - */ - public function count() { - return count($this->messages); - } - - /** - * Removes all previously stored messages. - */ - public function clear() { - $this->messages = array(); - } -} +debugging( + 'The message_sink.php file is deprecated and will be removed in Moodle 6.0. ' . + 'It should not be manually included. ' . + 'The class can now be autoloaded from \core\test\phpunit\message_sink.', + DEBUG_DEVELOPER +); diff --git a/public/lib/phpunit/classes/phpmailer_sink.php b/public/lib/phpunit/classes/phpmailer_sink.php index df0eb015afd..0a676b26699 100644 --- a/public/lib/phpunit/classes/phpmailer_sink.php +++ b/public/lib/phpunit/classes/phpmailer_sink.php @@ -23,65 +23,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -/** - * phpmailer message sink. - * - * @package core - * @category phpunit - * @copyright 2013 Andrew Nicols - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class phpunit_phpmailer_sink { - /** - * @var array of records which would have been sent by phpmailer. - */ - protected $messages = array(); - - /** - * Stop message redirection. - * - * Use if you do not want message redirected any more. - */ - public function close() { - phpunit_util::stop_phpmailer_redirection(); - } - - /** - * To be called from phpunit_util only! - * - * @param stdClass $message record from messages table - */ - public function add_message($message) { - /* Number messages from 0. */ - $this->messages[] = $message; - } - - /** - * Returns all redirected messages. - * - * The instances are records from the messages table. - * The array indexes are numbered from 0 and the order is matching - * the creation of events. - * - * @return array - */ - public function get_messages() { - return $this->messages; - } - - /** - * Return number of messages redirected to this sink. - * @return int - */ - public function count() { - return count($this->messages); - } - - /** - * Removes all previously stored messages. - */ - public function clear() { - $this->messages = array(); - } -} +debugging( + 'The phpmailer_sink.php file is deprecated and will be removed in Moodle 6.0. ' . + 'It should not be manually included. ' . + 'The class can now be autoloaded from \core\test\phpunit\phpmailer_sink.', + DEBUG_DEVELOPER +); diff --git a/public/lib/phpunit/classes/phpunit_dataset.php b/public/lib/phpunit/classes/phpunit_dataset.php index 7339c87f8a4..03b0e1a6f3a 100644 --- a/public/lib/phpunit/classes/phpunit_dataset.php +++ b/public/lib/phpunit/classes/phpunit_dataset.php @@ -32,6 +32,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + declare(strict_types=1); /** @@ -202,7 +203,7 @@ class phpunit_dataset { } } - $structure = phpunit_util::get_tablestructure(); + $structure = \core\test\phpunit\phpunit_util::get_tablestructure(); foreach ($this->tables as $table) { // Apply filter. diff --git a/public/lib/phpunit/classes/util.php b/public/lib/phpunit/classes/util.php index c6b2e3c1ccf..8d42c7c3f52 100644 --- a/public/lib/phpunit/classes/util.php +++ b/public/lib/phpunit/classes/util.php @@ -15,1066 +15,16 @@ // along with Moodle. If not, see . /** - * Utility class. - * - * @package core - * @copyright 2012 Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -// phpcs:disable moodle.Commenting.ValidTags.Invalid -// phpcs:disable moodle.PHP.ForbiddenFunctions.FoundWithAlternative -// phpcs:disable moodle.Files.MoodleInternal.MoodleInternalGlobalState - -use core\di; -use core\hook; - -require_once(__DIR__ . '/../../testing/classes/util.php'); -require_once(__DIR__ . "/coverage_info.php"); - -/** - * Collection of utility methods. + * A collection of utility methods for PHPUnit. * * @package core * @copyright Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class phpunit_util extends testing_util { - /** - * @var int last value of db writes counter, used for db resetting - */ - public static $lastdbwrites = null; - /** @var array An array of original globals, restored after each test */ - protected static $globals = []; - - /** @var array list of debugging messages triggered during the last test execution */ - protected static $debuggings = []; - - /** @var phpunit_message_sink alternative target for moodle messaging */ - protected static $messagesink = null; - - /** @var phpunit_phpmailer_sink alternative target for phpmailer messaging */ - protected static $phpmailersink = null; - - /** @var phpunit_message_sink alternative target for moodle messaging */ - protected static $eventsink = null; - - /** - * @var array Files to skip when resetting dataroot folder - */ - protected static $datarootskiponreset = ['.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess']; - - /** - * @var array Files to skip when dropping dataroot folder - */ - protected static $datarootskipondrop = ['.', '..', 'lock']; - - /** - * Load global $CFG; - * - * @internal - */ - public static function initialise_cfg() { - global $DB; - - $dbhash = false; - try { - $dbhash = $DB->get_field('config', 'value', ['name' => 'phpunittest']); - } catch (Exception $e) { - // Not installed yet. - initialise_cfg(); - return; - } - if ($dbhash !== core_component::get_all_versions_hash()) { - // Do not set CFG - the only way forward is to drop and reinstall. - return; - } - // Standard CFG init. - initialise_cfg(); - } - - /** - * Reset contents of all database tables to initial values, reset caches, etc. - * - * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care! - * - * @param bool $detectchanges - * true - changes in global state and database are reported as errors - * false - no errors reported - * null - only critical problems are reported as errors - */ - public static function reset_all_data($detectchanges = false) { - global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $FULLME, $FILTERLIB_PRIVATE; - - // Stop all hook redirections. - di::get(hook\manager::class)->phpunit_stop_redirections(); - - // Stop any message redirection. - self::stop_message_redirection(); - - // Stop any message redirection. - self::stop_event_redirection(); - - // Start a new email redirection. - // This will clear any existing phpmailer redirection. - // We redirect all phpmailer output to this message sink which is - // called instead of phpmailer actually sending the message. - self::start_phpmailer_redirection(); - - // We used to call gc_collect_cycles here to ensure desctructors were called between tests. - // This accounted for 25% of the total time running phpunit - so we removed it. - - // Show any unhandled debugging messages, the runbare() could already reset it. - self::display_debugging_messages(); - self::reset_debugging(); - - // Reset global $DB in case somebody mocked it. - $DB = self::get_global_backup('DB'); - - if ($DB->is_transaction_started()) { - // We can not reset inside transaction. - $DB->force_transaction_rollback(); - } - - $resetdb = self::reset_database(); - $localename = self::get_locale_name(); - $warnings = []; - - if ($detectchanges === true) { - if ($resetdb) { - $warnings[] = 'Warning: unexpected database modification, resetting DB state'; - } - - $oldcfg = self::get_global_backup('CFG'); - $oldsite = self::get_global_backup('SITE'); - foreach ($CFG as $k => $v) { - if (!property_exists($oldcfg, $k)) { - $warnings[] = 'Warning: unexpected new $CFG->' . $k . ' value'; - } else if ($oldcfg->$k !== $CFG->$k) { - $warnings[] = 'Warning: unexpected change of $CFG->' . $k . ' value'; - } - unset($oldcfg->$k); - } - if ($oldcfg) { - foreach ($oldcfg as $k => $v) { - $warnings[] = 'Warning: unexpected removal of $CFG->' . $k; - } - } - - if ($USER->id != 0) { - $warnings[] = 'Warning: unexpected change of $USER'; - } - - if ($COURSE->id != $oldsite->id) { - $warnings[] = 'Warning: unexpected change of $COURSE'; - } - - if ($FULLME !== self::get_global_backup('FULLME')) { - $warnings[] = 'Warning: unexpected change of $FULLME'; - } - - if (setlocale(LC_TIME, 0) !== $localename) { - $warnings[] = 'Warning: unexpected change of locale'; - } - } - - if (ini_get('max_execution_time') != 0) { - // This is special warning for all resets because we do not want any - // libraries to mess with timeouts unintentionally. - // Our PHPUnit integration is not supposed to change it either. - - if ($detectchanges !== false) { - $warnings[] = 'Warning: max_execution_time was changed to ' . ini_get('max_execution_time'); - } - set_time_limit(0); - } - - // Restore original globals. - $_SERVER = self::get_global_backup('_SERVER'); - $CFG = self::get_global_backup('CFG'); - $SITE = self::get_global_backup('SITE'); - $FULLME = self::get_global_backup('FULLME'); - $_GET = []; - $_POST = []; - $_FILES = []; - $_REQUEST = []; - $COURSE = $SITE; - - // Reinitialise following globals. - $OUTPUT = new bootstrap_renderer(); - $PAGE = new moodle_page(); - \navigation_node::reset_all_data(); - $FULLME = null; - $ME = null; - $SCRIPT = null; - $FILTERLIB_PRIVATE = null; - if (!empty($SESSION->notifications)) { - $SESSION->notifications = []; - } - - // Empty sessison and set fresh new not-logged-in user. - \core\session\manager::init_empty_session(); - - // Reset all static caches. - \core\event\manager::phpunit_reset(); - accesslib_clear_all_caches(true); - accesslib_reset_role_cache(); - get_string_manager()->reset_caches(true); - reset_text_filters_cache(true); - get_message_processors(false, true, true); - filter_manager::reset_caches(); - core_filetypes::reset_caches(); - \core_search\manager::clear_static(); - core_user::reset_caches(); - \core\output\icon_system::reset_caches(); - if (class_exists('core_media_manager', false)) { - core_media_manager::reset_caches(); - } - - // Reset static unit test options. - if (class_exists('\availability_date\condition', false)) { - \availability_date\condition::set_current_time_for_test(0); - } - - // Reset internal users. - core_user::reset_internal_users(); - - // Clear static caches in calendar container. - if (class_exists('\core_calendar\local\event\container', false)) { - core_calendar\local\event\container::reset_caches(); - } - - // Reset hub registration caches. - \core\hub\registration::reset_caches(); - - // TODO MDL-25290: add more resets here and probably refactor them to new core function. - - // Reset course and module caches. - core_courseformat\base::reset_course_cache(0); - get_fast_modinfo(0, 0, true); - - // Reset other singletons. - if (class_exists('core_plugin_manager')) { - core_plugin_manager::reset_caches(true); - } - if (class_exists('\core\update\checker')) { - \core\update\checker::reset_caches(true); - } - if (class_exists('\core_course\customfield\course_handler')) { - \core_course\customfield\course_handler::reset_caches(); - } - if (class_exists('\core_reportbuilder\manager')) { - \core_reportbuilder\manager::reset_caches(); - } - if (class_exists('\core_cohort\customfield\cohort_handler')) { - \core_cohort\customfield\cohort_handler::reset_caches(); - } - if (class_exists('\core_group\customfield\group_handler')) { - \core_group\customfield\group_handler::reset_caches(); - } - if (class_exists('\core_group\customfield\grouping_handler')) { - \core_group\customfield\grouping_handler::reset_caches(); - } - if (class_exists('\core_reportbuilder\customfield\report_handler')) { - \core_reportbuilder\customfield\report_handler::reset_caches(); - } - if (class_exists('\core_customfield\customfield\shared_handler')) { - \core_customfield\customfield\shared_handler::reset_caches(); - } - - // Clear static cache within restore. - if (class_exists('restore_section_structure_step')) { - restore_section_structure_step::reset_caches(); - } - - // Purge dataroot directory. - self::reset_dataroot(); - - // Restore original config once more in case resetting of caches changed CFG. - $CFG = self::get_global_backup('CFG'); - - // Inform data generator. - self::get_data_generator()->reset(); - - // Fix PHP settings. - error_reporting($CFG->debug); - - // Reset the date/time class. - core_date::phpunit_reset(); - - // Make sure the time locale is consistent - that is Australian English. - setlocale(LC_TIME, $localename); - - // Reset the log manager cache. - get_log_manager(true); - - // Reset user agent. - core_useragent::instance(true, null); - - // Reset the DI container. - \core\di::reset_container(); - - // Verify db writes just in case something goes wrong in reset. - if (self::$lastdbwrites != $DB->perf_get_writes()) { - error_log('Unexpected DB writes in phpunit_util::reset_all_data()'); - self::$lastdbwrites = $DB->perf_get_writes(); - } - - if ($warnings) { - $warnings = implode("\n", $warnings); - throw new \core_phpunit\exception\test_exception($warnings); - } - } - - /** - * Reset all database tables to default values. - * - * @return bool true if reset done, false if skipped - */ - public static function reset_database() { - global $DB; - - if (defined('PHPUNIT_ISOLATED_TEST') && PHPUNIT_ISOLATED_TEST && self::$lastdbwrites === null) { - // This is an isolated test and the lastdbwrites has not yet been initialised. - // Isolated test runs are reset by the test runner before the run starts. - self::$lastdbwrites = $DB->perf_get_writes(); - } - - if (!is_null(self::$lastdbwrites) && self::$lastdbwrites == $DB->perf_get_writes()) { - return false; - } - - if (!parent::reset_database()) { - return false; - } - - self::$lastdbwrites = $DB->perf_get_writes(); - - return true; - } - - /** - * Called during bootstrap only! - * @internal - * - * @return void - */ - public static function bootstrap_init() { - global $CFG, $SITE, $DB, $FULLME; - - // Backup the globals. - self::$globals['_SERVER'] = $_SERVER; - self::$globals['CFG'] = clone($CFG); - self::$globals['SITE'] = clone($SITE); - self::$globals['DB'] = $DB; - self::$globals['FULLME'] = $FULLME; - - // Refresh data in all tables, clear caches, etc. - self::reset_all_data(); - } - - /** - * Print some Moodle related info to console. - * - * @internal - */ - public static function bootstrap_moodle_info() { - echo self::get_site_info(); - } - - /** - * Returns original state of global variable. - * - * @param string $name - * @return mixed - */ - public static function get_global_backup($name) { - if ($name === 'DB') { - // No cloning of database object, - // We just need the original reference, not original state. - return self::$globals['DB']; - } - if (isset(self::$globals[$name])) { - if (is_object(self::$globals[$name])) { - $return = clone(self::$globals[$name]); - return $return; - } else { - return self::$globals[$name]; - } - } - return null; - } - - /** - * Is this site initialised to run unit tests? - * - * @return array errorcode=>message, 0 means ok - */ - public static function testing_ready_problem() { - global $DB; - - $localename = self::get_locale_name(); - if (setlocale(LC_TIME, $localename) === false) { - return [PHPUNIT_EXITCODE_CONFIGERROR, "Required locale '$localename' is not installed."]; - } - - if (!self::is_test_site()) { - // Dataroot was verified in bootstrap, so it must be DB. - return [PHPUNIT_EXITCODE_CONFIGERROR, 'Can not use database for testing, try different prefix']; - } - - $tables = $DB->get_tables(false); - if (empty($tables)) { - return [PHPUNIT_EXITCODE_INSTALL, '']; - } - - if (!self::is_test_data_updated()) { - return [PHPUNIT_EXITCODE_REINSTALL, '']; - } - - return [0, '']; - } - - /** - * Drop all test site data. - * - * Note: To be used from CLI scripts only. - * - * @param bool $displayprogress if true, this method will echo progress information. - * @return void may terminate execution with exit code - */ - public static function drop_site($displayprogress = false) { - global $DB, $CFG; - - if (!self::is_test_site()) { - phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!'); - } - - // Purge dataroot. - if ($displayprogress) { - echo "Purging dataroot:\n"; - } - - self::reset_dataroot(); - testing_initdataroot($CFG->dataroot, 'phpunit'); - - // Drop all tables. - self::drop_database($displayprogress); - - // Drop dataroot. - self::drop_dataroot(); - } - - /** - * Perform a fresh test site installation - * - * Note: To be used from CLI scripts only. - * - * @return void may terminate execution with exit code - */ - public static function install_site() { - global $DB, $CFG; - - if (!self::is_test_site()) { - phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not install on non-test site!!'); - } - - if ($DB->get_tables()) { - [$errorcode, $message] = self::testing_ready_problem(); - if ($errorcode) { - phpunit_bootstrap_error( - PHPUNIT_EXITCODE_REINSTALL, - 'Database tables already present, Moodle PHPUnit test environment can not be initialised', - ); - } else { - phpunit_bootstrap_error(0, 'Moodle PHPUnit test environment is already initialised'); - } - } - - $options = []; - $options['adminpass'] = 'admin'; - $options['shortname'] = 'phpunit'; - $options['fullname'] = 'PHPUnit test site'; - - install_cli_database($options, false); - - // Set the admin email address. - $DB->set_field('user', 'email', 'admin@example.com', ['username' => 'admin']); - - // Disable all logging for performance and sanity reasons. - set_config('enabled_stores', '', 'tool_log'); - - // Remove any default blocked hosts and port restrictions, to avoid blocking tests (eg those using local files). - set_config('curlsecurityblockedhosts', ''); - set_config('curlsecurityallowedport', ''); - - // Execute all the adhoc tasks. - while ($task = \core\task\manager::get_next_adhoc_task(time())) { - $task->execute(); - \core\task\manager::adhoc_task_complete($task); - } - - // We need to keep the installed dataroot filedir files. - // So each time we reset the dataroot before running a test, the default files are still installed. - self::save_original_data_files(); - - // Store version hash in the database and in a file. - self::store_versions_hash(); - - // Store database data and structure. - self::store_database_state(); - } - - /** - * Builds root/phpunit.xml file using defaults from /phpunit.xml.dist - * - * @return bool true means main config file created, false means only dataroot file created - */ - public static function build_config_file() { - global $CFG; - - $template = << - public/@dir@ - public/@dir@/classes - public/@dir@/fixtures - - - EOF; - $data = file_get_contents("$CFG->root/phpunit.xml.dist"); - - $suites = ''; - $includelists = []; - $excludelists = []; - - $subsystems = core_component::get_core_subsystems(); - $subsystems['core'] = $CFG->dirroot . '/lib'; - foreach ($subsystems as $subsystem => $fulldir) { - if (empty($fulldir)) { - continue; - } - if (!file_exists("{$fulldir}/tests/")) { - // There are no tests - skip this directory. - continue; - } - - $dir = substr($fulldir, strlen($CFG->dirroot) + 1); - if ($coverageinfo = self::get_coverage_info($fulldir)) { - $includelists = array_merge($includelists, $coverageinfo->get_includelists("public/{$dir}")); - $excludelists = array_merge($excludelists, $coverageinfo->get_excludelists("public/{$dir}")); - } - } - - $plugintypes = core_component::get_plugin_types(); - ksort($plugintypes); - foreach (array_keys($plugintypes) as $type) { - $plugs = core_component::get_plugin_list($type); - ksort($plugs); - foreach ($plugs as $plug => $plugindir) { - if (!file_exists("{$plugindir}/tests/")) { - // There are no tests - skip this directory. - continue; - } - - $dir = substr($plugindir, strlen($CFG->dirroot) + 1); - $testdir = "{$dir}/tests"; - $component = "{$type}_{$plug}"; - - $suite = str_replace('@component@', $component, $template); - $suite = str_replace('@dir@', $testdir, $suite); - - $suites .= $suite; - - if ($coverageinfo = self::get_coverage_info($plugindir)) { - $includelists = array_merge($includelists, $coverageinfo->get_includelists("public/{$dir}")); - $excludelists = array_merge($excludelists, $coverageinfo->get_excludelists("public/{$dir}")); - } - } - } - - // Start a sequence between 100000 and 199000 to ensure each call to init produces - // different ids in the database. This reduces the risk that hard coded values will - // end up being placed in phpunit or behat test code. - $sequencestart = 100000 + mt_rand(0, 99) * 1000; - - $data = preg_replace('| *.*|s', trim($suites, "\n"), $data, 1); - $data = str_replace( - '', - '', - $data - ); - - $coverages = self::get_coverage_config($includelists, $excludelists); - $data = preg_replace('| *|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"); - } - } - - return (bool)$result; - } - - /** - * Builds phpunit.xml files for all components using defaults from /phpunit.xml.dist - * - * @return void, stops if can not write files - */ - public static function build_component_config_files() { - global $CFG; - - $template = << - - tests - tests/classes - tests/fixtures - - - EOT; - $coveragedefault = << - . - - - tests - - EOT; - - // Start a sequence between 100000 and 199000 to ensure each call to init produces - // different ids in the database. This reduces the risk that hard coded values will - // end up being placed in phpunit or behat test code. - $sequencestart = 100000 + mt_rand(0, 99) * 1000; - - // Use the upstream file as source for the distributed configurations. - $ftemplate = file_get_contents("$CFG->root/phpunit.xml.dist"); - $ftemplate = preg_replace('| *', $ftemplate); - - // Gets all the components with tests. - $components = tests_finder::get_components_with_tests('phpunit'); - - // Create the corresponding phpunit.xml file for each component. - foreach ($components as $cname => $cpath) { - // Calculate the component suite. - $ctemplate = $template; - $ctemplate = str_replace('@component@', $cname, $ctemplate); - - $fcontents = str_replace('', $ctemplate, $ftemplate); - - // Check for coverage configurations. - if ($coverageinfo = self::get_coverage_info($cpath)) { - $coverages = self::get_coverage_config($coverageinfo->get_includelists(''), $coverageinfo->get_excludelists('')); - } else { - $coverages = $coveragedefault; - } - $fcontents = preg_replace('| *|s', trim($coverages, "\n"), $fcontents); - - // Apply it to the file template. - $fcontents = str_replace( - '', - '', - $fcontents - ); - - // Fix link to schema. - $level = substr_count(str_replace('\\', '/', $cpath), '/') - substr_count(str_replace('\\', '/', $CFG->dirroot), '/'); - $fcontents = str_replace('public/lib/phpunit/', str_repeat('../', $level) . 'lib/phpunit/', $fcontents); - - // Write the file. - $result = false; - if (is_writable($cpath)) { - if ($result = (bool)file_put_contents("$cpath/phpunit.xml", $fcontents)) { - testing_fix_file_permissions("$cpath/phpunit.xml"); - } - } - // Problems writing file, throw error. - if (!$result) { - phpunit_bootstrap_error( - PHPUNIT_EXITCODE_CONFIGWARNING, - "Can not create $cpath/phpunit.xml configuration file, verify dir permissions", - ); - } - } - } - - /** - * To be called from debugging() only. - * @param string $message - * @param int $level - * @param string $from - */ - public static function debugging_triggered($message, $level, $from) { - // Store only if debugging triggered from actual test, - // we need normal debugging outside of tests to find problems in our phpunit integration. - $backtrace = debug_backtrace(); - - // Only for advanced_testcase, database_driver_testcase (and descendants). Others aren't - // able to manage the debugging sink, so any debugging has to be output normally and, hopefully, - // PHPUnit execution will catch that unexpected output properly. - $sinksupport = false; - foreach ($backtrace as $bt) { - if ( - isset($bt['object']) - && is_object($bt['object']) - && ( - $bt['object'] instanceof advanced_testcase - || $bt['object'] instanceof database_driver_testcase) - ) { - $sinksupport = true; - break; - } - } - if (!$sinksupport) { - return false; - } - - // Verify that we are inside a PHPUnit test (little bit redundant, because - // we already have checked above that this is an advanced/database_driver - // testcase, but let's keep things double safe for now). - foreach ($backtrace as $bt) { - if ( - isset($bt['object']) && is_object($bt['object']) - && $bt['object'] instanceof PHPUnit\Framework\TestCase - ) { - $debug = new stdClass(); - $debug->message = $message; - $debug->level = $level; - $debug->from = $from; - - self::$debuggings[] = $debug; - - return true; - } - } - return false; - } - - /** - * Resets the list of debugging messages. - */ - public static function reset_debugging() { - self::$debuggings = []; - set_debugging(DEBUG_DEVELOPER); - } - - /** - * Returns all debugging messages triggered during test. - * @return array with instances having message, level and stacktrace property. - */ - public static function get_debugging_messages() { - return self::$debuggings; - } - - /** - * Prints out any debug messages accumulated during test execution. - * - * @param bool $return true to return the messages or false to print them directly. Default false. - * @return bool|string false if no debug messages, true if debug triggered or string of messages - */ - public static function display_debugging_messages($return = false) { - if (empty(self::$debuggings)) { - return false; - } - - $debugstring = ''; - foreach (self::$debuggings as $debug) { - $debugstring .= 'Debugging: ' . $debug->message . "\n" . trim($debug->from) . "\n"; - } - - if ($return) { - return $debugstring; - } - echo $debugstring; - return true; - } - - /** - * Start message redirection. - * - * Note: Do not call directly from tests, - * use $sink = $this->redirectMessages() instead. - * - * @return phpunit_message_sink - */ - public static function start_message_redirection() { - if (self::$messagesink) { - self::stop_message_redirection(); - } - self::$messagesink = new phpunit_message_sink(); - return self::$messagesink; - } - - /** - * End message redirection. - * - * Note: Do not call directly from tests, - * use $sink->close() instead. - */ - public static function stop_message_redirection() { - self::$messagesink = null; - } - - /** - * Are messages redirected to some sink? - * - * Note: to be called from messagelib.php only! - * - * @return bool - */ - public static function is_redirecting_messages() { - return !empty(self::$messagesink); - } - - /** - * To be called from messagelib.php only! - * - * @param stdClass $message record from messages table - * @return bool true means send message, false means message "sent" to sink. - */ - public static function message_sent($message) { - if (self::$messagesink) { - self::$messagesink->add_message($message); - } - } - - /** - * Start phpmailer redirection. - * - * Note: Do not call directly from tests, - * use $sink = $this->redirectEmails() instead. - * - * @return phpunit_phpmailer_sink - */ - public static function start_phpmailer_redirection() { - if (self::$phpmailersink) { - // If an existing mailer sink is active, just clear it. - self::$phpmailersink->clear(); - } else { - self::$phpmailersink = new phpunit_phpmailer_sink(); - } - return self::$phpmailersink; - } - - /** - * End phpmailer redirection. - * - * Note: Do not call directly from tests, - * use $sink->close() instead. - */ - public static function stop_phpmailer_redirection() { - self::$phpmailersink = null; - } - - /** - * Are messages for phpmailer redirected to some sink? - * - * Note: to be called from moodle_phpmailer.php only! - * - * @return bool - */ - public static function is_redirecting_phpmailer() { - return !empty(self::$phpmailersink); - } - - /** - * To be called from messagelib.php only! - * - * @param stdClass $message record from messages table - * @return bool true means send message, false means message "sent" to sink. - */ - public static function phpmailer_sent($message) { - if (self::$phpmailersink) { - self::$phpmailersink->add_message($message); - } - } - - /** - * Start event redirection. - * - * @private - * Note: Do not call directly from tests, - * use $sink = $this->redirectEvents() instead. - * - * @return phpunit_event_sink - */ - public static function start_event_redirection() { - if (self::$eventsink) { - self::stop_event_redirection(); - } - self::$eventsink = new phpunit_event_sink(); - return self::$eventsink; - } - - /** - * End event redirection. - * - * @private - * Note: Do not call directly from tests, - * use $sink->close() instead. - */ - public static function stop_event_redirection() { - self::$eventsink = null; - } - - /** - * Are events redirected to some sink? - * - * Note: to be called from \core\event\base only! - * - * @private - * @return bool - */ - public static function is_redirecting_events() { - return !empty(self::$eventsink); - } - - /** - * To be called from \core\event\base only! - * - * @private - * @param \core\event\base $event record from event_read table - * @return bool true means send event, false means event "sent" to sink. - */ - public static function event_triggered(\core\event\base $event) { - if (self::$eventsink) { - self::$eventsink->add_event($event); - } - } - - /** - * Gets the name of the locale for testing environment (Australian English) - * depending on platform environment. - * - * @return string the locale name. - */ - protected static function get_locale_name() { - global $CFG; - if ($CFG->ostype === 'WINDOWS') { - return 'English_Australia.1252'; - } else { - return 'en_AU.UTF-8'; - } - } - - /** - * Executes all adhoc tasks in the queue. Useful for testing asynchronous behaviour. - * - * @return void - */ - public static function run_all_adhoc_tasks() { - $now = time(); - while (($task = \core\task\manager::get_next_adhoc_task($now)) !== null) { - try { - $task->execute(); - \core\task\manager::adhoc_task_complete($task); - } catch (Exception $e) { - \core\task\manager::adhoc_task_failed($task); - } - } - } - - /** - * Helper function to call a protected/private method of an object using reflection. - * - * Example 1. Calling a protected object method: - * $result = call_internal_method($myobject, 'method_name', [$param1, $param2], '\my\namespace\myobjectclassname'); - * - * Example 2. Calling a protected static method: - * $result = call_internal_method(null, 'method_name', [$param1, $param2], '\my\namespace\myclassname'); - * - * @param object|null $object the object on which to call the method, or null if calling a static method. - * @param string $methodname the name of the protected/private method. - * @param array $params the array of function params to pass to the method. - * @param string $classname the fully namespaced name of the class the object was created from (base in the case of mocks), - * or the name of the static class when calling a static method. - * @return mixed the respective return value of the method. - */ - public static function call_internal_method($object, $methodname, array $params, $classname) { - $reflection = new \ReflectionClass($classname); - $method = $reflection->getMethod($methodname); - return $method->invokeArgs($object, $params); - } - - /** - * Pad the supplied string with $level levels of indentation. - * - * @param string $string The string to pad - * @param int $level The number of levels of indentation to pad - * @return string - */ - protected static function pad(string $string, int $level): string { - return str_repeat(" ", $level * 2) . "{$string}\n"; - } - - /** - * Normalise any text to always use unix line endings (line-feeds). - * - * @param string $text The text to normalize - * @return string - */ - public static function normalise_line_endings(string $text): string { - return str_replace(["\r\n", "\r"], "\n", $text); - } - - /** - * Get the coverage config for the supplied includelist and excludelist configuration. - * - * @param string[] $includelists The list of files/folders in the includelist. - * @param string[] $excludelists The list of files/folders in the excludelist. - * @return string - */ - protected static function get_coverage_config(array $includelists, array $excludelists): string { - $coverages = ''; - if (!empty($includelists)) { - $coverages .= self::pad("", 2); - foreach ($includelists as $line) { - $coverages .= self::pad($line, 3); - } - $coverages .= self::pad("", 2); - if (!empty($excludelists)) { - $coverages .= self::pad("", 2); - foreach ($excludelists as $line) { - $coverages .= self::pad($line, 3); - } - $coverages .= self::pad("", 2); - } - } - - return $coverages; - } - - /** - * Get the phpunit_coverage_info for the specified plugin or subsystem directory. - * - * @param string $fulldir The directory to find the coverage info file in. - * @return phpunit_coverage_info - */ - protected static function get_coverage_info(string $fulldir): phpunit_coverage_info { - $coverageconfig = "{$fulldir}/tests/coverage.php"; - if (file_exists($coverageconfig)) { - $coverageinfo = require($coverageconfig); - if (!$coverageinfo instanceof phpunit_coverage_info) { - throw new \coding_exception("{$coverageconfig} does not return a phpunit_coverage_info"); - } - - return $coverageinfo; - } - - return new phpunit_coverage_info(); - } - - /** - * Whether the current process is an isolated test process. - * - * @return bool - */ - public static function is_in_isolated_process(): bool { - // Note: There is no function to call, or much to go by in order to tell whether we are in an isolated process - // during Bootstrap, when this function is called. - // We can do so by testing the existence of the wrapper function, but there is nothing set until that point. - return function_exists('__phpunit_run_isolated_test'); - } -} +debugging( + 'The util.php file is deprecated and will be removed in Moodle 6.0. ' . + 'It should not be manually included. ' . + 'The class can now be autoloaded from \core\test\phpunit\phpunit_util.', + DEBUG_DEVELOPER +); diff --git a/public/lib/phpunit/lib.php b/public/lib/phpunit/lib.php index d8ac8b78991..55fcaca8f03 100644 --- a/public/lib/phpunit/lib.php +++ b/public/lib/phpunit/lib.php @@ -25,16 +25,10 @@ // NOTE: MOODLE_INTERNAL is not verified here because we load this before setup.php! -require_once(__DIR__.'/classes/util.php'); require_once(__DIR__.'/classes/phpunit_dataset.php'); -require_once(__DIR__.'/classes/event_mock.php'); -require_once(__DIR__.'/classes/event_sink.php'); -require_once(__DIR__.'/classes/message_sink.php'); -require_once(__DIR__.'/classes/phpmailer_sink.php'); require_once(__DIR__.'/classes/base_testcase.php'); require_once(__DIR__.'/classes/basic_testcase.php'); require_once(__DIR__.'/classes/database_driver_testcase.php'); require_once(__DIR__.'/classes/advanced_testcase.php'); require_once(__DIR__.'/classes/constraint_object_is_equal_with_exceptions.php'); require_once(__DIR__.'/../testing/classes/test_lock.php'); -require_once(__DIR__.'/../testing/classes/tests_finder.php'); diff --git a/public/lib/phpunit/tests/advanced_test.php b/public/lib/phpunit/tests/advanced_test.php index 999fe963d4b..bc99f5d0bac 100644 --- a/public/lib/phpunit/tests/advanced_test.php +++ b/public/lib/phpunit/tests/advanced_test.php @@ -455,7 +455,7 @@ final class advanced_test extends \advanced_testcase { $this->assertEquals(count($processors1) + 1, count($processors3)); } - public function test_message_redirection(): \phpunit_message_sink { + public function test_message_redirection(): \core\test\phpunit\message_sink { $this->preventResetByRollback(); // Messaging is not compatible with transactions... $this->resetAfterTest(false); @@ -570,14 +570,14 @@ final class advanced_test extends \advanced_testcase { $this->assertEquals(1, $sink->count()); // Test if sink can be carried over to next test. - $this->assertTrue(\phpunit_util::is_redirecting_messages()); + $this->assertTrue(\core\test\phpunit\phpunit_util::is_redirecting_messages()); return $sink; } /** * @depends test_message_redirection */ - public function test_message_redirection_noreset(\phpunit_message_sink $sink): void { + public function test_message_redirection_noreset(\core\test\phpunit\message_sink $sink): void { if ($this->isInIsolation()) { $this->markTestSkipped('State cannot be carried over between tests in isolated tests'); } @@ -585,7 +585,7 @@ final class advanced_test extends \advanced_testcase { $this->preventResetByRollback(); // Messaging is not compatible with transactions... $this->resetAfterTest(); - $this->assertTrue(\phpunit_util::is_redirecting_messages()); + $this->assertTrue(\core\test\phpunit\phpunit_util::is_redirecting_messages()); $this->assertEquals(1, $sink->count()); $message = new \core\message\message(); @@ -609,7 +609,7 @@ final class advanced_test extends \advanced_testcase { * @depends test_message_redirection_noreset */ public function test_message_redirection_reset(): void { - $this->assertFalse(\phpunit_util::is_redirecting_messages(), 'Test reset must stop message redirection.'); + $this->assertFalse(\core\test\phpunit\phpunit_util::is_redirecting_messages(), 'Test reset must stop message redirection.'); } public function test_set_timezone(): void { diff --git a/public/lib/phpunit/tests/basic_test.php b/public/lib/phpunit/tests/basic_test.php index 192372acce1..05d6e3f0476 100644 --- a/public/lib/phpunit/tests/basic_test.php +++ b/public/lib/phpunit/tests/basic_test.php @@ -16,7 +16,7 @@ namespace core; -use phpunit_util; +use core\test\phpunit\phpunit_util; /** * Test basic_testcase extra features and PHPUnit Moodle integration. @@ -27,7 +27,7 @@ use phpunit_util; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\PHPUnit\Framework\Attributes\CoversClass(\basic_testcase::class)] -#[\PHPUnit\Framework\Attributes\CoversClass(\phpunit_util::class)] +#[\PHPUnit\Framework\Attributes\CoversClass(phpunit_util::class)] final class basic_test extends \basic_testcase { /** @var bool */ protected bool $testassertexecuted = false; diff --git a/public/lib/phpunit/tests/util_test.php b/public/lib/phpunit/tests/util_test.php index 7951179a7e7..eefcc3957b1 100644 --- a/public/lib/phpunit/tests/util_test.php +++ b/public/lib/phpunit/tests/util_test.php @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -namespace core; +namespace core\test\phpunit; /** * Test util extra features. @@ -29,10 +29,10 @@ final class util_test extends \advanced_testcase { * @dataProvider set_table_modified_by_sql_provider */ public function test_set_table_modified_by_sql($sql, $expectations): void { - \phpunit_util::reset_updated_table_list(); - \phpunit_util::set_table_modified_by_sql($sql); + phpunit_util::reset_updated_table_list(); + phpunit_util::set_table_modified_by_sql($sql); foreach ($expectations as $table => $present) { - $this->assertEquals($present, !empty(\phpunit_util::$tableupdated[$table])); + $this->assertEquals($present, !empty(phpunit_util::$tableupdated[$table])); } } diff --git a/public/lib/setup.php b/public/lib/setup.php index 69004da98e7..17aaf0189df 100644 --- a/public/lib/setup.php +++ b/public/lib/setup.php @@ -688,7 +688,7 @@ setup_DB(); if (PHPUNIT_TEST and !PHPUNIT_UTIL) { // Make sure tests do not run in parallel. $suffix = ''; - if (phpunit_util::is_in_isolated_process()) { + if (\core\test\phpunit\phpunit_util::is_in_isolated_process()) { $suffix = '.isolated'; } test_lock::acquire('phpunit', $suffix); @@ -696,7 +696,7 @@ if (PHPUNIT_TEST and !PHPUNIT_UTIL) { try { if ($dbhash = $DB->get_field('config', 'value', array('name'=>'phpunittest'))) { // reset DB tables - phpunit_util::reset_database(); + \core\test\phpunit\phpunit_util::reset_database(); } } catch (Exception $e) { if ($dbhash) { @@ -736,7 +736,7 @@ if (is_readable($bootstraplocalfile)) { // Load up any configuration from the config table or MUC cache. if (PHPUNIT_TEST) { - phpunit_util::initialise_cfg(); + \core\test\phpunit\phpunit_util::initialise_cfg(); } else { initialise_cfg(); } diff --git a/public/lib/testing/classes/tests_finder.php b/public/lib/testing/classes/tests_finder.php index b93ef233fd0..fdacfb957b9 100644 --- a/public/lib/testing/classes/tests_finder.php +++ b/public/lib/testing/classes/tests_finder.php @@ -14,15 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Tests finder - * - * @package core - * @category test - * @copyright 2012 Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - /** * Finds components and plugins with tests * @@ -31,177 +22,10 @@ * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class tests_finder { - /** - * Returns all the components with tests of the specified type - * @param string $testtype The kind of test we are looking for - * @return array - */ - public static function get_components_with_tests($testtype) { - - // Get all the components - $components = self::get_all_plugins_with_tests($testtype) + self::get_all_subsystems_with_tests($testtype); - - // Get all the directories having tests - $directories = self::get_all_directories_with_tests($testtype); - - // Find any directory not covered by proper components - $remaining = array_diff($directories, $components); - - // Add them to the list of components - $components += $remaining; - - return $components; - } - - /** - * Returns all the plugins having tests - * @param string $testtype The kind of test we are looking for - * @return array all the plugins having tests - */ - private static function get_all_plugins_with_tests($testtype) { - $pluginswithtests = array(); - - $plugintypes = core_component::get_plugin_types(); - ksort($plugintypes); - foreach ($plugintypes as $type => $unused) { - $plugs = core_component::get_plugin_list($type); - ksort($plugs); - foreach ($plugs as $plug => $fullplug) { - // Look for tests recursively - if (self::directory_has_tests($fullplug, $testtype)) { - $pluginswithtests[$type . '_' . $plug] = $fullplug; - } - } - } - return $pluginswithtests; - } - - /** - * Returns all the subsystems having tests - * - * Note we are hacking here the list of subsystems - * to cover some well-known subsystems that are not properly - * returned by the {@link get_core_subsystems()} function. - * - * @param string $testtype The kind of test we are looking for - * @return array all the subsystems having tests - */ - private static function get_all_subsystems_with_tests($testtype) { - global $CFG; - - $subsystemswithtests = array(); - - $subsystems = core_component::get_core_subsystems(); - - // Hack the list a bit to cover some well-known ones - $subsystems['backup'] = $CFG->dirroot.'/backup'; - $subsystems['db-dml'] = $CFG->dirroot.'/lib/dml'; - $subsystems['db-ddl'] = $CFG->dirroot.'/lib/ddl'; - - ksort($subsystems); - foreach ($subsystems as $subsys => $fullsubsys) { - if ($fullsubsys === null) { - continue; - } - if (!is_dir($fullsubsys)) { - continue; - } - // Look for tests recursively - if (self::directory_has_tests($fullsubsys, $testtype)) { - $subsystemswithtests['core_' . $subsys] = $fullsubsys; - } - } - return $subsystemswithtests; - } - - /** - * Returns all the directories having tests - * - * @param string $testtype The kind of test we are looking for - * @return array all directories having tests - */ - private static function get_all_directories_with_tests($testtype) { - global $CFG; - - // List of directories to exclude from test file searching. - $excludedir = array('node_modules', 'vendor'); - - // Get first level directories in which tests should be searched. - $directoriestosearch = array(); - $alldirs = glob($CFG->dirroot . DIRECTORY_SEPARATOR . '*' , GLOB_ONLYDIR); - foreach ($alldirs as $dir) { - if (!in_array(basename($dir), $excludedir) && (filetype($dir) != 'link')) { - $directoriestosearch[] = $dir; - } - } - - // Search for tests in valid directories. - $dirs = array(); - foreach ($directoriestosearch as $dir) { - $dirite = new RecursiveDirectoryIterator($dir); - $iteite = new RecursiveIteratorIterator($dirite); - $regexp = self::get_regexp($testtype); - $regite = new RegexIterator($iteite, $regexp); - foreach ($regite as $path => $element) { - $key = dirname(dirname($path)); - $value = trim(str_replace(DIRECTORY_SEPARATOR, '_', str_replace($CFG->dirroot, '', $key)), '_'); - $dirs[$key] = $value; - } - } - ksort($dirs); - return array_flip($dirs); - } - - /** - * Returns if a given directory has tests (recursively) - * - * @param string $dir full path to the directory to look for phpunit tests - * @param string $testtype phpunit|behat - * @return bool if a given directory has tests (true) or no (false) - */ - private static function directory_has_tests($dir, $testtype) { - if (!is_dir($dir)) { - return false; - } - - $dirite = new RecursiveDirectoryIterator($dir); - $iteite = new RecursiveIteratorIterator($dirite); - $regexp = self::get_regexp($testtype); - $regite = new RegexIterator($iteite, $regexp); - $regite->rewind(); - if ($regite->valid()) { - return true; - } - return false; - } - - - /** - * Returns the regular expression to match by the test files - * @param string $testtype - * @return string - */ - private static function get_regexp($testtype) { - - $sep = preg_quote(DIRECTORY_SEPARATOR, '|'); - - switch ($testtype) { - case 'phpunit': - $regexp = '|'.$sep.'tests'.$sep.'.*_test\.php$|'; - break; - case 'features': - $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'.*\.feature$|'; - break; - case 'stepsdefinitions': - $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'behat_.*\.php$|'; - break; - case 'behat': - $regexp = '!'.$sep.'tests'.$sep.'behat'.$sep.'(.*\.feature)|(behat_.*\.php)$!'; - break; - } - - return $regexp; - } -} +debugging( + 'The tests_finder.php file is deprecated and will be removed in Moodle 6.0. ' . + 'It should not be manually included. ' . + 'The class can now be autoloaded from \core\test\test_finder.', + DEBUG_DEVELOPER +); diff --git a/public/lib/tests/classes/phpunit/data_provider_finished_subscriber.php b/public/lib/tests/classes/phpunit/data_provider_finished_subscriber.php index 162f11e5e05..0a6a5a2dea5 100644 --- a/public/lib/tests/classes/phpunit/data_provider_finished_subscriber.php +++ b/public/lib/tests/classes/phpunit/data_provider_finished_subscriber.php @@ -47,7 +47,7 @@ final class data_provider_finished_subscriber implements DataProviderMethodFinis } if ($resetall) { - \phpunit_util::reset_all_data(); + \core\test\phpunit\phpunit_util::reset_all_data(); $this->update_db_writes(); } } diff --git a/public/lib/tests/coverage.php b/public/lib/tests/coverage.php index fb9176cc6d0..ebfa2a20bc1 100644 --- a/public/lib/tests/coverage.php +++ b/public/lib/tests/coverage.php @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -defined('MOODLE_INTERNAL') || die(); - /** * Coverage information for the core subsystem. * @@ -24,14 +22,7 @@ defined('MOODLE_INTERNAL') || die(); * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -/** - * Coverage information for the core subsystem. - * - * @copyright 2018 Andrew Nicols - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -return new class extends phpunit_coverage_info { +return new class extends \core\test\phpunit\coverage_info { /** @var array The list of folders relative to the plugin root to include in coverage generation. */ protected $includelistfolders = [ // This is a legacy hangup which relates to parts of the file storage API being placed in the wrong location. diff --git a/public/lib/tests/curl_security_helper_test.php b/public/lib/tests/curl_security_helper_test.php index 4c390778b44..c4fb6e1eceb 100644 --- a/public/lib/tests/curl_security_helper_test.php +++ b/public/lib/tests/curl_security_helper_test.php @@ -203,8 +203,15 @@ final class curl_security_helper_test extends \advanced_testcase { $this->resetAfterTest(true); $helper = new \core\files\curl_security_helper(); set_config('curlsecurityblockedhosts', $blockedhosts); - $this->assertEquals($expected, \phpunit_util::call_internal_method($helper, 'host_is_blocked', [$host], - '\core\files\curl_security_helper')); + $this->assertEquals( + $expected, + \core\test\phpunit\phpunit_util::call_internal_method( + $helper, + 'host_is_blocked', + [$host], + \core\files\curl_security_helper::class, + ), + ); } /** @@ -257,8 +264,15 @@ final class curl_security_helper_test extends \advanced_testcase { $this->resetAfterTest(true); $helper = new \core\files\curl_security_helper(); set_config('curlsecurityallowedport', $allowedports); - $this->assertEquals($expected, \phpunit_util::call_internal_method($helper, 'port_is_blocked', [$port], - '\core\files\curl_security_helper')); + $this->assertEquals( + $expected, + \core\test\phpunit\phpunit_util::call_internal_method( + $helper, + 'port_is_blocked', + [$port], + \core\files\curl_security_helper::class, + ), + ); } /** diff --git a/public/lib/tests/message_test.php b/public/lib/tests/message_test.php index 40290cf067d..15fa10cba23 100644 --- a/public/lib/tests/message_test.php +++ b/public/lib/tests/message_test.php @@ -261,8 +261,8 @@ final class message_test extends \advanced_testcase { /** * Test get_messages_by_component method. * - * @covers \phpunit_message_sink::get_messages_by_component - * @covers \phpunit_message_sink::get_messages_by_component_and_type + * @covers \core\test\phpunit\message_sink::get_messages_by_component + * @covers \core\test\phpunit\message_sink::get_messages_by_component_and_type */ public function test_get_messages_by_component(): void { $this->resetAfterTest(); diff --git a/public/lib/tests/output/mustache_template_source_loader_test.php b/public/lib/tests/output/mustache_template_source_loader_test.php index ef7c8a7cb27..63dd274c23c 100644 --- a/public/lib/tests/output/mustache_template_source_loader_test.php +++ b/public/lib/tests/output/mustache_template_source_loader_test.php @@ -122,7 +122,7 @@ TBD; TBC; $loader = new mustache_template_source_loader(); - $actual = \phpunit_util::call_internal_method( + $actual = \core\test\phpunit\phpunit_util::call_internal_method( $loader, 'strip_template_comments', [$templatewithcomment], @@ -494,7 +494,7 @@ TEMPLATE; * @param string $expected The expected output */ public function test_scan_template_source_for_dependencies($loader, $source, $expected): void { - $actual = \phpunit_util::call_internal_method( + $actual = \core\test\phpunit\phpunit_util::call_internal_method( $loader, 'scan_template_source_for_dependencies', [$source], diff --git a/public/lib/tests/scss_test.php b/public/lib/tests/scss_test.php index 54c603217f4..70284388291 100644 --- a/public/lib/tests/scss_test.php +++ b/public/lib/tests/scss_test.php @@ -123,7 +123,7 @@ CSS */ public function test_is_valid_file($path, $valid): void { $scss = new \core_scss(); - $pathvalid = \phpunit_util::call_internal_method($scss, 'is_valid_file', [$path], \core_scss::class); + $pathvalid = \core\test\phpunit\phpunit_util::call_internal_method($scss, 'is_valid_file', [$path], \core_scss::class); $this->assertSame($valid, $pathvalid); } diff --git a/public/lib/weblib.php b/public/lib/weblib.php index 3d9d3f8a225..ca734fd98ed 100644 --- a/public/lib/weblib.php +++ b/public/lib/weblib.php @@ -2505,7 +2505,7 @@ function debugging($message = '', $level = DEBUG_NORMAL, $backtrace = null) { } $from = format_backtrace($backtrace, CLI_SCRIPT || NO_DEBUG_DISPLAY); if (PHPUNIT_TEST) { - if (phpunit_util::debugging_triggered($message, $level, $from)) { + if (\core\test\phpunit\phpunit_util::debugging_triggered($message, $level, $from)) { // We are inside test, the debug message was logged. return true; } diff --git a/public/lib/xhprof/xhprof_moodle.php b/public/lib/xhprof/xhprof_moodle.php index 1a14acc0748..f476709d5e2 100644 --- a/public/lib/xhprof/xhprof_moodle.php +++ b/public/lib/xhprof/xhprof_moodle.php @@ -30,7 +30,6 @@ require_once($CFG->libdir . '/xhprof/xhprof_lib/utils/xhprof_runs.php'); require_once($CFG->libdir . '/tablelib.php'); require_once($CFG->libdir . '/setuplib.php'); require_once($CFG->libdir . '/filelib.php'); -require_once($CFG->libdir . '/phpunit/classes/util.php'); require_once($CFG->dirroot . '/backup/util/xml/xml_writer.class.php'); require_once($CFG->dirroot . '/backup/util/xml/output/xml_output.class.php'); require_once($CFG->dirroot . '/backup/util/xml/output/file_xml_output.class.php'); @@ -679,7 +678,7 @@ function profiling_export_generate(array $runids, $tmpdir) { $release = $CFG->release; $version = $CFG->version; $dbtype = $CFG->dbtype; - $githash = phpunit_util::get_git_hash(); + $githash = \core\test\phpunit\phpunit_util::get_git_hash(); $date = time(); // Create the xml output and writer for the main file. diff --git a/public/message/output/popup/tests/api_test.php b/public/message/output/popup/tests/api_test.php index 256b0d60acd..08999f39611 100644 --- a/public/message/output/popup/tests/api_test.php +++ b/public/message/output/popup/tests/api_test.php @@ -34,7 +34,7 @@ require_once($CFG->dirroot . '/message/output/popup/tests/base.php'); final class api_test extends \advanced_testcase { use \message_popup_test_helper; - /** @var \phpunit_message_sink message redirection. */ + /** @var \core\test\phpunit\message_sink message redirection. */ public $messagesink; /** diff --git a/public/message/output/popup/tests/externallib_test.php b/public/message/output/popup/tests/externallib_test.php index f8b64d6f49d..a111facc059 100644 --- a/public/message/output/popup/tests/externallib_test.php +++ b/public/message/output/popup/tests/externallib_test.php @@ -37,7 +37,7 @@ require_once($CFG->dirroot . '/message/output/popup/tests/base.php'); final class externallib_test extends \advanced_testcase { use message_popup_test_helper; - /** @var \phpunit_message_sink message redirection. */ + /** @var \core\test\phpunit\message_sink message redirection. */ public $messagesink; /** diff --git a/public/mod/assign/tests/locallib_test.php b/public/mod/assign/tests/locallib_test.php index e16ea121258..731f68d6b96 100644 --- a/public/mod/assign/tests/locallib_test.php +++ b/public/mod/assign/tests/locallib_test.php @@ -2157,7 +2157,7 @@ You can see it appended to your . -defined('MOODLE_INTERNAL') || die(); - /** * Coverage information for the core_question. * * @copyright 2022 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -return new class extends phpunit_coverage_info { +return new class extends \core\test\phpunit\coverage_info { /** @var array The list of files relative to the plugin root to include in coverage generation. */ protected $includelistfiles = [ 'category_class.php', diff --git a/public/question/type/numerical/tests/coverage.php b/public/question/type/numerical/tests/coverage.php index 8d8e9355908..67f7913844c 100644 --- a/public/question/type/numerical/tests/coverage.php +++ b/public/question/type/numerical/tests/coverage.php @@ -14,15 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -defined('MOODLE_INTERNAL') || die(); - /** * Coverage information for the qtype_numerical. * * @copyright 2022 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -return new class extends phpunit_coverage_info { +return new class extends \core\test\phpunit\coverage_info { /** @var array The list of files relative to the plugin root to include in coverage generation. */ protected $includelistfiles = [ 'question.php', diff --git a/public/question/type/ordering/tests/coverage.php b/public/question/type/ordering/tests/coverage.php index 9eec85ca75c..43ff011f65b 100644 --- a/public/question/type/ordering/tests/coverage.php +++ b/public/question/type/ordering/tests/coverage.php @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -defined('MOODLE_INTERNAL') || die(); - /** * Coverage information for the qtype_ordering subsystem. * @@ -23,7 +21,7 @@ defined('MOODLE_INTERNAL') || die(); * @copyright 2023 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -return new class extends phpunit_coverage_info { +return new class extends \core\test\phpunit\coverage_info { /** @var array The list of folders relative to the plugin root to include in coverage generation. */ protected $includelistfolders = [ 'backup', diff --git a/public/question/type/ordering/tests/questiontype_test.php b/public/question/type/ordering/tests/questiontype_test.php index 82cfce5f732..721cad7849b 100644 --- a/public/question/type/ordering/tests/questiontype_test.php +++ b/public/question/type/ordering/tests/questiontype_test.php @@ -16,8 +16,7 @@ namespace qtype_ordering; -use core_question_generator; -use phpunit_util; +use core\test\phpunit\phpunit_util; use qtype_ordering; use qtype_ordering_test_helper; use qtype_ordering_edit_form; diff --git a/public/repository/nextcloud/tests/lib_test.php b/public/repository/nextcloud/tests/lib_test.php index d7b418b0a6b..d1642fda480 100644 --- a/public/repository/nextcloud/tests/lib_test.php +++ b/public/repository/nextcloud/tests/lib_test.php @@ -23,10 +23,9 @@ */ namespace repository_nextcloud; -use PHPUnit\Framework\MockObject\MockObject; -use repository; use repository_nextcloud; use webdav_client; +use PHPUnit\Framework\MockObject\MockObject; defined('MOODLE_INTERNAL') || die(); @@ -615,7 +614,7 @@ JSON; $oauthmock->expects($this->once())->method('get_accesstoken')->will($this->returnValue($fakeaccesstoken)); $this->set_private_property($oauthmock, 'client'); - $dav = \phpunit_util::call_internal_method($this->repo, "initiate_webdavclient", [], 'repository_nextcloud'); + $dav = \core\test\phpunit\phpunit_util::call_internal_method($this->repo, "initiate_webdavclient", [], 'repository_nextcloud'); // Verify that port is set correctly (private property). $refclient = new \ReflectionClass($dav); diff --git a/public/user/tests/coverage.php b/public/user/tests/coverage.php index 876c23f6de5..4e43b5f1fd6 100644 --- a/public/user/tests/coverage.php +++ b/public/user/tests/coverage.php @@ -20,7 +20,7 @@ * @copyright 2021 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -return new class extends phpunit_coverage_info { +return new class extends \core\test\phpunit\coverage_info { /** @var array The list of files relative to the plugin root to include in coverage generation. */ protected $includelistfiles = [ 'editlib.php', From 9142bd0ac6c7b94edd9968a39773e616acdd049b Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 7 Jan 2026 12:00:31 +0800 Subject: [PATCH 2/7] 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); } From 563242cf826a688ca51cf6e0d71ff966e77e7395 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 28 Jan 2026 21:30:10 +0800 Subject: [PATCH 3/7] MDL-87716 core: Update coding style Update coding style for files moved to namespaced locations. --- .../classes/test/phpunit/coverage_info.php | 5 +- .../lib/classes/test/phpunit/event_mock.php | 2 +- .../lib/classes/test/phpunit/event_sink.php | 7 ++- .../lib/classes/test/phpunit/message_sink.php | 8 ++-- .../classes/test/phpunit/phpmailer_sink.php | 4 +- public/lib/classes/test/test_finder.php | 48 ++++++++++--------- public/lib/classes/test/testing_util.php | 32 +++++-------- 7 files changed, 48 insertions(+), 58 deletions(-) diff --git a/public/lib/classes/test/phpunit/coverage_info.php b/public/lib/classes/test/phpunit/coverage_info.php index ddcaa0bf834..2eb3aa77f34 100644 --- a/public/lib/classes/test/phpunit/coverage_info.php +++ b/public/lib/classes/test/phpunit/coverage_info.php @@ -22,12 +22,11 @@ use core\output\html_writer; * Coverage information for PHPUnit. * * @package core - * @category phpunit + * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class coverage_info { - /** @var array The list of folders relative to the plugin root to include in coverage generation. */ protected $includelistfolders = []; @@ -68,7 +67,7 @@ class coverage_info { $includelistfolders = array_merge([ 'classes', 'tests/generator', - ], $this->includelistfolders);; + ], $this->includelistfolders); $includelistfiles = array_merge([ 'externallib.php', diff --git a/public/lib/classes/test/phpunit/event_mock.php b/public/lib/classes/test/phpunit/event_mock.php index eddbde23057..41ac3c6ffa8 100644 --- a/public/lib/classes/test/phpunit/event_mock.php +++ b/public/lib/classes/test/phpunit/event_mock.php @@ -20,7 +20,7 @@ namespace core\test\phpunit; * Event mock class. * * @package core - * @category phpunit + * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/public/lib/classes/test/phpunit/event_sink.php b/public/lib/classes/test/phpunit/event_sink.php index d24aaf7fea7..d1c1d941477 100644 --- a/public/lib/classes/test/phpunit/event_sink.php +++ b/public/lib/classes/test/phpunit/event_sink.php @@ -20,13 +20,13 @@ namespace core\test\phpunit; * Event redirection sink. * * @package core - * @category phpunit + * @category test * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_sink { /** @var \core\event\base[] array of events */ - protected $events = array(); + protected $events = []; /** * Stop event redirection. @@ -40,7 +40,6 @@ class event_sink { /** * To be called from phpunit_util only! * - * @private * @param \core\event\base $event record from event_read table */ public function add_event(\core\event\base $event) { @@ -74,7 +73,7 @@ class event_sink { * Removes all previously stored events. */ public function clear() { - $this->events = array(); + $this->events = []; } } diff --git a/public/lib/classes/test/phpunit/message_sink.php b/public/lib/classes/test/phpunit/message_sink.php index f4ad1b47266..0312d31d2a3 100644 --- a/public/lib/classes/test/phpunit/message_sink.php +++ b/public/lib/classes/test/phpunit/message_sink.php @@ -20,13 +20,13 @@ namespace core\test\phpunit; * Message sink. * * @package core - * @category phpunit + * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class message_sink { /** @var array of records from messages table */ - protected $messages = array(); + protected $messages = []; /** * Stop message redirection. @@ -89,7 +89,7 @@ class message_sink { string $component, string $type, ): array { - return array_filter($this->get_messages_by_component($component), function($message) use ($type) { + return array_filter($this->get_messages_by_component($component), function ($message) use ($type) { return $message->eventtype == $type; }); } @@ -106,7 +106,7 @@ class message_sink { * Removes all previously stored messages. */ public function clear() { - $this->messages = array(); + $this->messages = []; } } diff --git a/public/lib/classes/test/phpunit/phpmailer_sink.php b/public/lib/classes/test/phpunit/phpmailer_sink.php index 478dbaa2292..6dd4e518b08 100644 --- a/public/lib/classes/test/phpunit/phpmailer_sink.php +++ b/public/lib/classes/test/phpunit/phpmailer_sink.php @@ -28,7 +28,7 @@ class phpmailer_sink { /** * @var array of records which would have been sent by phpmailer. */ - protected $messages = array(); + protected $messages = []; /** * Stop message redirection. @@ -74,7 +74,7 @@ class phpmailer_sink { * Removes all previously stored messages. */ public function clear() { - $this->messages = array(); + $this->messages = []; } } diff --git a/public/lib/classes/test/test_finder.php b/public/lib/classes/test/test_finder.php index cec2a1e80da..c198c022433 100644 --- a/public/lib/classes/test/test_finder.php +++ b/public/lib/classes/test/test_finder.php @@ -17,7 +17,7 @@ namespace core\test; /** - * Finds components and plugins with tests + * Finds components and plugins with tests. * * @package core * @category test @@ -26,34 +26,36 @@ namespace core\test; */ class test_finder { /** - * Returns all the components with tests of the specified type + * Returns all the components with tests of the specified type. + * * @param string $testtype The kind of test we are looking for * @return array */ public static function get_components_with_tests($testtype) { - // Get all the components + // Get all the components. $components = self::get_all_plugins_with_tests($testtype) + self::get_all_subsystems_with_tests($testtype); - // Get all the directories having tests + // Get all the directories having tests. $directories = self::get_all_directories_with_tests($testtype); - // Find any directory not covered by proper components + // Find any directory not covered by proper components. $remaining = array_diff($directories, $components); - // Add them to the list of components + // Add them to the list of components. $components += $remaining; return $components; } /** - * Returns all the plugins having tests + * Returns all the plugins having tests. + * * @param string $testtype The kind of test we are looking for * @return array all the plugins having tests */ private static function get_all_plugins_with_tests($testtype) { - $pluginswithtests = array(); + $pluginswithtests = []; $plugintypes = \core\component::get_plugin_types(); ksort($plugintypes); @@ -61,7 +63,7 @@ class test_finder { $plugs = \core\component::get_plugin_list($type); ksort($plugs); foreach ($plugs as $plug => $fullplug) { - // Look for tests recursively + // Look for tests recursively. if (self::directory_has_tests($fullplug, $testtype)) { $pluginswithtests[$type . '_' . $plug] = $fullplug; } @@ -83,14 +85,14 @@ class test_finder { private static function get_all_subsystems_with_tests($testtype) { global $CFG; - $subsystemswithtests = array(); + $subsystemswithtests = []; $subsystems = \core\component::get_core_subsystems(); - // Hack the list a bit to cover some well-known ones - $subsystems['backup'] = $CFG->dirroot.'/backup'; - $subsystems['db-dml'] = $CFG->dirroot.'/lib/dml'; - $subsystems['db-ddl'] = $CFG->dirroot.'/lib/ddl'; + // Hack the list a bit to cover some well-known ones. + $subsystems['backup'] = $CFG->dirroot . '/backup'; + $subsystems['db-dml'] = $CFG->dirroot . '/lib/dml'; + $subsystems['db-ddl'] = $CFG->dirroot . '/lib/ddl'; ksort($subsystems); foreach ($subsystems as $subsys => $fullsubsys) { @@ -100,7 +102,7 @@ class test_finder { if (!is_dir($fullsubsys)) { continue; } - // Look for tests recursively + // Look for tests recursively. if (self::directory_has_tests($fullsubsys, $testtype)) { $subsystemswithtests['core_' . $subsys] = $fullsubsys; } @@ -118,11 +120,11 @@ class test_finder { global $CFG; // List of directories to exclude from test file searching. - $excludedir = array('node_modules', 'vendor'); + $excludedir = ['node_modules', 'vendor']; // Get first level directories in which tests should be searched. - $directoriestosearch = array(); - $alldirs = glob($CFG->dirroot . DIRECTORY_SEPARATOR . '*' , GLOB_ONLYDIR); + $directoriestosearch = []; + $alldirs = glob($CFG->dirroot . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR); foreach ($alldirs as $dir) { if (!in_array(basename($dir), $excludedir) && (filetype($dir) != 'link')) { $directoriestosearch[] = $dir; @@ -130,7 +132,7 @@ class test_finder { } // Search for tests in valid directories. - $dirs = array(); + $dirs = []; foreach ($directoriestosearch as $dir) { $dirite = new \RecursiveDirectoryIterator($dir); $iteite = new \RecursiveIteratorIterator($dirite); @@ -181,16 +183,16 @@ class test_finder { switch ($testtype) { case 'phpunit': - $regexp = '|'.$sep.'tests'.$sep.'.*_test\.php$|'; + $regexp = '|' . $sep . 'tests' . $sep . '.*_test\.php$|'; break; case 'features': - $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'.*\.feature$|'; + $regexp = '|' . $sep . 'tests' . $sep . 'behat' . $sep . '.*\.feature$|'; break; case 'stepsdefinitions': - $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'behat_.*\.php$|'; + $regexp = '|' . $sep . 'tests' . $sep . 'behat' . $sep . 'behat_.*\.php$|'; break; case 'behat': - $regexp = '!'.$sep.'tests'.$sep.'behat'.$sep.'(.*\.feature)|(behat_.*\.php)$!'; + $regexp = '!' . $sep . 'tests' . $sep . 'behat' . $sep . '(.*\.feature)|(behat_.*\.php)$!'; break; } diff --git a/public/lib/classes/test/testing_util.php b/public/lib/classes/test/testing_util.php index d02c99af67c..9d09ae4e513 100644 --- a/public/lib/classes/test/testing_util.php +++ b/public/lib/classes/test/testing_util.php @@ -73,7 +73,7 @@ abstract class testing_util { private static $originaldatafilesjson = 'originaldatafiles.json'; /** - * @var boolean set to true once $originaldatafilesjson file is created. + * @var bool set to true once $originaldatafilesjson file is created. */ private static $originaldatafilesjsonadded = false; @@ -90,7 +90,6 @@ abstract class testing_util { /** * Return the name of the JSON file containing the init filenames. * - * @static * @return string */ public static function get_originaldatafilesjson() { @@ -100,7 +99,6 @@ abstract class testing_util { /** * Return the dataroot. It's useful when mocking the dataroot when unit testing this class itself. * - * @static * @return string the dataroot. */ public static function get_dataroot() { @@ -118,7 +116,6 @@ abstract class testing_util { * Set the dataroot. It's useful when mocking the dataroot when unit testing this class itself. * * @param string $dataroot the dataroot of the test framework. - * @static */ public static function set_dataroot($dataroot) { self::$dataroot = $dataroot; @@ -126,7 +123,7 @@ abstract class testing_util { /** * Returns the testing framework name - * @static + * * @return string */ protected static function get_framework() { @@ -145,7 +142,7 @@ abstract class testing_util { /** * Get data generator - * @static + * * @return \testing_data_generator */ public static function get_data_generator() { @@ -162,7 +159,6 @@ abstract class testing_util { * Does this site (db and dataroot) appear to be used for production? * We try very hard to prevent accidental damage done to production servers!! * - * @static * @return bool */ public static function is_test_site() { @@ -280,7 +276,7 @@ abstract class testing_util { /** * Returns contents of all tables right after installation. - * @static + * * @return array $table=>$records */ protected static function get_tabledata() { @@ -309,7 +305,7 @@ abstract class testing_util { /** * Returns structure of all tables right after installation. - * @static + * * @return array $table=>$records */ public static function get_tablestructure() { @@ -338,7 +334,7 @@ abstract class testing_util { /** * Returns the names of sequences for each autoincrementing id field in all standard tables. - * @static + * * @return array $table=>$sequencename */ public static function get_sequencenames() { @@ -366,7 +362,7 @@ abstract class testing_util { /** * Returns list of tables that are unmodified and empty. * - * @static + * * @return array of table names, empty if unknown */ protected static function guess_unmodified_empty_tables() { @@ -421,7 +417,6 @@ abstract class testing_util { /** * Determine the next unique starting id sequences. * - * @static * @param array $records The records to use to determine the starting value for the table. * @param string $table table name. * @return int The value the sequence should be set to. @@ -450,9 +445,7 @@ abstract class testing_util { /** * Reset all database sequences to initial values. * - * @static * @param array $empties tables that are known to be unmodified and empty - * @return void */ public static function reset_all_database_sequences(?array $empties = null) { global $DB; @@ -554,7 +547,7 @@ abstract class testing_util { /** * Reset all database tables to default values. - * @static + * * @return bool true if reset done, false if skipped */ public static function reset_database() { @@ -658,9 +651,7 @@ abstract class testing_util { } /** - * Purge dataroot directory - * @static - * @return void + * Purge dataroot directory. */ public static function reset_dataroot() { global $CFG; @@ -855,7 +846,7 @@ abstract class testing_util { /** * Drop the whole test database - * @static + * * @param bool $displayprogress */ protected static function drop_database($displayprogress = false) { @@ -893,8 +884,7 @@ abstract class testing_util { } /** - * Drops the test framework dataroot - * @static + * Drops the test framework dataroot. */ protected static function drop_dataroot() { global $CFG; From 7bd997e97c5877346d7d9850d4f708b2b02c2aec Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 29 Jan 2026 07:28:29 +0800 Subject: [PATCH 4/7] MDL-87716 testing: Stop installing composer if in a composer install --- public/lib/testing/lib.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/public/lib/testing/lib.php b/public/lib/testing/lib.php index 568674c22fe..92379db5155 100644 --- a/public/lib/testing/lib.php +++ b/public/lib/testing/lib.php @@ -184,6 +184,19 @@ function testing_error($errorcode, $text = '') { * @param bool $updatedependencies Upgrade dependencies */ function testing_update_composer_dependencies(bool $selfupdate = true, bool $updatedependencies = true): void { + // Check if we are running inside another project that already loaded composer. + if (class_exists(\Composer\InstalledVersions::class)) { + // The Composer library is already loaded. + // Check to see if Moodle is the root package. + $rootpackage = \Composer\InstalledVersions::getRootPackage(); + if ($rootpackage['name'] !== 'moodle/moodle') { + // Assume that the user has already run the composer commands manually. + echo " Composer library is already loaded by another project ({$rootpackage['name']}).\n"; + echo " Skipping composer self-update and dependency update.\n"; + return; + } + } + // To restore the value after finishing. $cwd = getcwd(); From c0081c5fc549f0a77c6c846e6dd330d9e3d2437b Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 29 Jan 2026 17:17:27 +0800 Subject: [PATCH 5/7] MDL-87716 core: Support Behat in Composer/Standard installation To support Behat we need to update the path to the vendor directory to be relative to the root package. We also need to update the `composer.json` to allow the BehatExtension to be autoloaded using the composer autoloader, which means moving it out of the dev autoloader. Whilst this is not ideal, it's currently unavoidable. --- composer.json | 6 ++- public/admin/tool/behat/cli/run.php | 2 +- .../admin/tool/behat/cli/util_single_run.php | 1 + public/lib/behat/classes/behat_command.php | 40 ++++++++++++------- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/composer.json b/composer.json index e5d3054aae8..d483054f260 100644 --- a/composer.json +++ b/composer.json @@ -20,10 +20,12 @@ "oleg-andreyev/mink-phpwebdriver": "1.3.*", "filp/whoops": "^2.15" }, - "autoload-dev": { + "autoload": { "psr-0": { "Moodle\\BehatExtension": "lib/behat/extension/" - }, + } + }, + "autoload-dev": { "psr-4": { "core_testing\\": "public/lib/testing/classes/", "core_phpunit\\": "public/lib/phpunit/classes/" diff --git a/public/admin/tool/behat/cli/run.php b/public/admin/tool/behat/cli/run.php index e7d346d62c0..b3bf79fe051 100644 --- a/public/admin/tool/behat/cli/run.php +++ b/public/admin/tool/behat/cli/run.php @@ -223,7 +223,7 @@ if (empty($parallelrun)) { $runtestscommand .= ' ' . $extraoptstr; $cmds['singlerun'] = $runtestscommand; - echo "Running single behat site:" . PHP_EOL; + echo "Running single behat site: with " . $runtestscommand . PHP_EOL; passthru("php $runtestscommand", $status); $exitcodes['singlerun'] = $status; chdir($cwd); diff --git a/public/admin/tool/behat/cli/util_single_run.php b/public/admin/tool/behat/cli/util_single_run.php index 79f3782f278..ae608d5cc8a 100644 --- a/public/admin/tool/behat/cli/util_single_run.php +++ b/public/admin/tool/behat/cli/util_single_run.php @@ -40,6 +40,7 @@ putenv('BEHAT_CLI=0'); // Basic functions. require_once(__DIR__ . '/../../../../lib/clilib.php'); require_once(__DIR__ . '/../../../../lib/behat/lib.php'); +require_once(dirname(__DIR__, 5) . '/vendor/autoload.php'); // CLI options. list($options, $unrecognized) = cli_get_params( diff --git a/public/lib/behat/classes/behat_command.php b/public/lib/behat/classes/behat_command.php index 8a5701cbd9d..49c2faf847c 100644 --- a/public/lib/behat/classes/behat_command.php +++ b/public/lib/behat/classes/behat_command.php @@ -119,24 +119,33 @@ class behat_command { } } - // If relative path then prefix relative path. + $command = []; + if ($absolutepath) { - $pathprefix = testing_cli_argument_path('/'); - if (!empty($pathprefix)) { - $pathprefix .= $separator; - } - } else { - $pathprefix = ''; + $command[] = realpath(\Composer\InstalledVersions::getRootPackage()['install_path']); + } + if ($parallerun) { + $command[] = rtrim(\core\test\testing_util::get_moodle_relative_to_root_package(), '/'); } - if (!$parallerun) { - $command = $pathprefix . 'vendor' . $separator . 'bin' . $separator . $exec; - } else { - $command = 'php ' . $pathprefix . 'admin' . $separator . 'tool' . $separator . 'behat' . $separator . 'cli' - . $separator . 'run.php'; + if ($parallerun) { + $command = [ + ...$command, + 'public', + 'admin', + 'tool', + 'behat', + 'cli', + 'run.php', + ]; + return 'php ' . implode($separator, $command); } - return $command; + // Not a parallel run. + // Return the vendor path without php command. + $command = [...$command, 'vendor', 'bin', $exec]; + return implode($separator, $command); + } /** @@ -151,7 +160,8 @@ class behat_command { global $CFG; $currentcwd = getcwd(); - chdir(dirname($CFG->dirroot)); + $rootpath = realpath(\Composer\InstalledVersions::getRootPackage()['install_path']); + chdir($rootpath); exec(self::get_behat_command() . ' ' . $options, $output, $code); chdir($currentcwd); @@ -237,7 +247,7 @@ class behat_command { * @return bool */ public static function are_behat_dependencies_installed() { - if (!is_dir(__DIR__ . '/../../../../vendor/behat')) { + if (!\Composer\InstalledVersions::isInstalled('behat/behat')) { return false; } return true; From 778d3e88c63d5765381cda3993b7a086b98adcde Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 29 Jan 2026 12:11:32 +0800 Subject: [PATCH 6/7] MDL-87716 core: Add GitHub Actions tests for PHPUnit --- .github/workflows/composed.yml | 117 +++++++++++++++++++++++ .github/workflows/composed/composer.json | 22 +++++ .github/workflows/composed/config.php | 67 +++++++++++++ 3 files changed, 206 insertions(+) create mode 100644 .github/workflows/composed.yml create mode 100644 .github/workflows/composed/composer.json create mode 100644 .github/workflows/composed/config.php diff --git a/.github/workflows/composed.yml b/.github/workflows/composed.yml new file mode 100644 index 00000000000..17ae25dcfc5 --- /dev/null +++ b/.github/workflows/composed.yml @@ -0,0 +1,117 @@ +name: Run tests in a Composed Moodle Setup + +on: + push: + branches-ignore: + - main + - MOODLE_[0-9]+_STABLE + tags-ignore: + - v[0-9]+.[0-9]+.[0-9]+* + workflow_dispatch: + inputs: + phpunit_extra_options: + description: Additional options to apply to PHPUnit + required: false + default: '' + +env: + php: 8.4 + +jobs: + PHPUnit: + runs-on: ${{ matrix.os }} + services: + exttests: + image: moodlehq/moodle-exttests + ports: + - 8080:80 + redis: + image: redis + ports: + - 6379:6379 + strategy: + fail-fast: false + matrix: + include: + # MySQL builds always run with the lowest PHP supported version. + - os: ubuntu-24.04 + php: 8.3 + extensions: + db: mysqli + # PostgreSQL builds always run with the highest PHP supported version. + - os: ubuntu-24.04 + php: 8.4 + db: pgsql + + steps: + - name: Setting up DB mysql + if: ${{ matrix.db == 'mysqli' }} + uses: moodlehq/mysql-action@v1 + with: + collation server: utf8mb4_bin + mysql version: 8.4 + mysql database: test + mysql user: test + mysql password: test + use tmpfs: true + tmpfs size: '1024M' + extra conf: --skip-log-bin + + - name: Setting up DB pgsql + if: ${{ matrix.db == 'pgsql' }} + uses: m4nu56/postgresql-action@v1 + with: + postgresql version: 16 + postgresql db: test + postgresql user: test + postgresql password: test + + - name: Configuring git vars + uses: rlespinasse/github-slug-action@v5 + + - name: Setting up PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: ${{ matrix.extensions }} + ini-values: max_input_vars=5000 + coverage: none + tools: composer + + - name: Checking out code from ${{ env.GITHUB_REF_SLUG }} + uses: actions/checkout@v6 + with: + path: 'moodle' + + - name: 'Preparing the environment' + run: | + echo "pathtophp=$(which php)" >> $GITHUB_ENV # Inject installed pathtophp to env. The template config needs it. + sudo locale-gen en_AU.UTF-8 + + - name: Create the new Composer Project + run: | + mkdir testsite + cp moodle/.github/workflows/composed/composer.json testsite/composer.json + cp moodle/.github/workflows/composed/config.php testsite/config.php + mkdir -p testsite/data/moodledata + mkdir -p testsite/data/phpunitdata + + - name: Install the Moodle test site + working-directory: testsite + run: | + composer install --no-interaction + + - name: Setting up PHPUnit + working-directory: testsite + env: + dbtype: ${{ matrix.db }} + run: | + php moodle/public/admin/tool/phpunit/cli/init.php --no-composer-self-update + + - name: Running PHPUnit tests + working-directory: testsite + env: + dbtype: ${{ matrix.db }} + phpunit_options: ${{ secrets.phpunit_options }} + run: | + vendor/bin/phpunit $phpunit_options ${{ inputs.phpunit_extra_options }} diff --git a/.github/workflows/composed/composer.json b/.github/workflows/composed/composer.json new file mode 100644 index 00000000000..9b91b0feee6 --- /dev/null +++ b/.github/workflows/composed/composer.json @@ -0,0 +1,22 @@ +{ + "type": "project", + "require": { + "moodle/composer-installer": "^1", + "moodle/moodle-composer-scaffold": "^1", + "moodle/moodle": "@dev", + "moodle/moodle-testing": "^1.0" + }, + "repositories": [ + { + "name": "moodleundertest", + "type": "path", + "url": "../moodle" + } + ], + "config": { + "allow-plugins": { + "moodle/composer-installer": true, + "moodle/moodle-composer-scaffold": true + } + } +} diff --git a/.github/workflows/composed/config.php b/.github/workflows/composed/config.php new file mode 100644 index 00000000000..18e29c64269 --- /dev/null +++ b/.github/workflows/composed/config.php @@ -0,0 +1,67 @@ +. + +/** + * Template configuraton file for GitHub Actions CI/CD using Composer. + * + * @package core + * @copyright 2020 onwards Eloy Lafuente (stronk7) {@link https://stronk7.com} + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// This cannot be used out from a GitHub Actions workflow, so just exit. +getenv('GITHUB_WORKFLOW') || die; // phpcs:ignore moodle.Files.MoodleInternal.MoodleInternalGlobalState + +unset($CFG); +global $CFG; +$CFG = new stdClass(); + +$CFG->dbtype = getenv('dbtype'); +$CFG->dblibrary = 'native'; +$CFG->dbhost = '127.0.0.1'; +$CFG->dbname = 'test'; +$CFG->dbuser = 'test'; +$CFG->dbpass = 'test'; +$CFG->prefix = 'm_'; +$CFG->dboptions = ['dbcollation' => 'utf8mb4_bin']; + +$host = 'localhost'; +$CFG->wwwroot = "http://{$host}"; +$CFG->dataroot = __DIR__ . '/data/moodledata'; +$CFG->admin = 'admin'; +$CFG->directorypermissions = 0777; + +// Debug options - possible to be controlled by flag in future. +$CFG->debug = (E_ALL); // DEBUG_DEVELOPER. +$CFG->debugdisplay = 1; +$CFG->debugstringids = 1; // Add strings=1 to url to get string ids. +$CFG->perfdebug = 15; +$CFG->debugpageinfo = 1; +$CFG->allowthemechangeonurl = 1; +$CFG->passwordpolicy = 0; +$CFG->cronclionly = 0; +$CFG->pathtophp = getenv('pathtophp'); + +$CFG->phpunit_dataroot = __DIR__ . '/data/phpunitdata'; +$CFG->phpunit_prefix = 't_'; + +$CFG->routerconfigured = true; + +define('TEST_EXTERNAL_FILES_HTTP_URL', 'http://localhost:8080'); +define('TEST_EXTERNAL_FILES_HTTPS_URL', 'http://localhost:8080'); + +define('TEST_SESSION_REDIS_HOST', 'localhost'); +define('TEST_CACHESTORE_REDIS_TESTSERVERS', 'localhost'); From 80be1c2ede557f7d081a0856663bc5cbc3567c71 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Fri, 30 Jan 2026 15:42:10 +0800 Subject: [PATCH 7/7] MDL-87716 core: Fix failing unit tests Some tests in core were using paths relative to the process CWD, which is not safe. The environment tests needed to be updated to use the Composer autoloader. --- public/lib/setuplib.php | 4 ++-- public/lib/tests/setuplib_test.php | 7 ++++++- public/mod/data/tests/entries_export_test.php | 9 +++++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/public/lib/setuplib.php b/public/lib/setuplib.php index 89a9338eaad..037fe6c1de4 100644 --- a/public/lib/setuplib.php +++ b/public/lib/setuplib.php @@ -472,7 +472,7 @@ function get_docs_url($path = null) { */ function format_backtrace($callers, $plaintext = false) { // Do not use $CFG->dirroot because it might not be available in destructors. - $dirroot = dirname(__DIR__, 2); + $dirroot = realpath(dirname(__DIR__, 2)); if (empty($callers)) { return ''; @@ -490,7 +490,7 @@ function format_backtrace($callers, $plaintext = false) { $line .= sprintf( 'line %d of %s', $caller['line'], - str_replace($dirroot, '', $caller['file']), + str_replace($dirroot, '', realpath($caller['file'])), ); if (isset($caller['function'])) { $line .= ': call to '; diff --git a/public/lib/tests/setuplib_test.php b/public/lib/tests/setuplib_test.php index 38d820565f9..353b7f92eb3 100644 --- a/public/lib/tests/setuplib_test.php +++ b/public/lib/tests/setuplib_test.php @@ -557,6 +557,11 @@ final class setuplib_test extends \advanced_testcase { $this->assertStringContainsString(' of /public/lib/tests/setuplib_test.php', $output); // And a vendor path. - $this->assertStringContainsString(' of /vendor/', $output); + $rootdir = realpath(\Composer\InstalledVersions::getRootPackage()['install_path']); + // If the vendor directory is within the $CFG->root it won't be present. + if (str_starts_with($rootdir, realpath(dirname(__DIR__, 3)))) { + $rootdir = ''; + } + $this->assertStringContainsString(" of {$rootdir}/vendor", $output); } } diff --git a/public/mod/data/tests/entries_export_test.php b/public/mod/data/tests/entries_export_test.php index 470e65834cd..8bfda0d392f 100644 --- a/public/mod/data/tests/entries_export_test.php +++ b/public/mod/data/tests/entries_export_test.php @@ -101,7 +101,8 @@ final class entries_export_test extends \advanced_testcase { * @covers \mod_data\local\exporter\utils::data_exportdata */ public function test_export_csv(): void { - global $DB; + global $CFG, $DB; + [ 'data' => $data, 'cm' => $cm, @@ -151,9 +152,9 @@ final class entries_export_test extends \advanced_testcase { // file stored in a file and picture field. // So we expect that this file has to have the same content as sample.png. // Also, the default value for the subdirectory in the zip archive containing the files is 'files/'. - 'files/samplefile.png' => 'public/mod/data/field/picture/pix/sample.png', - 'files/samplefile_1.png' => 'public/mod/data/field/picture/pix/sample.png', - 'files/picturefile.png' => 'public/mod/data/field/picture/pix/sample.png', + 'files/samplefile.png' => "{$CFG->root}/public/mod/data/field/picture/pix/sample.png", + 'files/samplefile_1.png' => "{$CFG->root}/public/mod/data/field/picture/pix/sample.png", + 'files/picturefile.png' => "{$CFG->root}/public/mod/data/field/picture/pix/sample.png", // By checking that the content of the exported csv is identical to the fixture file it is verified // that the filenames in the csv file correspond to the names of the exported file. // It also verifies that files with identical file names in different fields (or records) will be numbered