diff --git a/lib/externallib.php b/lib/externallib.php index 8358fc21204..b17375c43cf 100644 --- a/lib/externallib.php +++ b/lib/externallib.php @@ -24,10 +24,16 @@ use core_external\util; +defined('MOODLE_INTERNAL') || die; + // Please note that this file and all of the classes and functions listed below will be deprecated from Moodle 4.6. // This deprecation is delayed to aid plugin developers when maintaining plugins for multiple Moodle versions. // See MDL-76583 for further information. +// If including this file for unit testing, it _must_ be run in an isolated process to prevent +// any side effect upon other tests. +require_phpunit_isolation(); + class_alias(\core_external\external_api::class, 'external_api'); class_alias(\core_external\restricted_context_exception::class, 'restricted_context_exception'); class_alias(\core_external\external_description::class, 'external_description'); diff --git a/lib/phpunit/classes/advanced_testcase.php b/lib/phpunit/classes/advanced_testcase.php index 048e04d420c..7e413cacf02 100644 --- a/lib/phpunit/classes/advanced_testcase.php +++ b/lib/phpunit/classes/advanced_testcase.php @@ -57,6 +57,20 @@ abstract class advanced_testcase extends base_testcase { $this->setBackupGlobals(false); $this->setBackupStaticAttributes(false); $this->setPreserveGlobalState(false); + + } + + /** + * Hook into the setInIsolation method to define an optional constant. + * + * @param bool $inisolation + */ + public function setInIsolation(bool $inisolation): void { + parent::setInIsolation($inisolation); + if ($inisolation) { + // Note: This is safe to do because it will only be set once per test run. + define('PHPUNIT_ISOLATED_TEST', true); + } } /** diff --git a/lib/setuplib.php b/lib/setuplib.php index 58a946b6cff..d87dc9dd457 100644 --- a/lib/setuplib.php +++ b/lib/setuplib.php @@ -2183,3 +2183,26 @@ function proxy_log_callback($code) { error_log($error . format_backtrace($trace, true)); // phpcs:ignore } } + +/** + * A helper function for deprecated files to use to ensure that, when they are included for unit tests, + * they are run in an isolated process. + * + * @throws \coding_exception The exception thrown when the process is not isolated. + */ +function require_phpunit_isolation(): void { + if (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST) { + // Not a test. + return; + } + + if (defined('PHPUNIT_ISOLATED_TEST')) { + // Already isolated. + return; + } + + throw new \coding_exception( + 'When including this file for a unit test, the test must be run in an isolated process. ' . + 'See the PHPUnit @runInSeparateProcess and @runTestsInSeparateProcesses annotations.' + ); +} diff --git a/lib/tests/setuplib_test.php b/lib/tests/setuplib_test.php index d2c65355291..2b25604b45b 100644 --- a/lib/tests/setuplib_test.php +++ b/lib/tests/setuplib_test.php @@ -520,4 +520,26 @@ class setuplib_test extends \advanced_testcase { $uuid = \core\uuid::generate(); $this->assertTrue(self::is_valid_uuid_v4($uuid), "Invalid v4 UUID: '$uuid'"); } + + /** + * Test require_phpunit_isolation in a test which is not isolated. + * + * @covers ::require_phpunit_isolation + */ + public function test_require_phpunit_isolation(): void { + // A unit test which is not isolated will throw a coding_exception when the function is called. + $this->expectException('coding_exception'); + require_phpunit_isolation(); + } + + /** + * Test require_phpunit_isolation in a test which is isolated. + * + * @covers ::require_phpunit_isolation + * @runInSeparateProcess + */ + public function test_require_phpunit_isolation_isolated(): void { + $this->expectNotToPerformAssertions(); + require_phpunit_isolation(); + } }