Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Advanced test case.
  19   *
  20   * @package    core
  21   * @category   phpunit
  22   * @copyright  2012 Petr Skoda {@link http://skodak.org}
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  
  27  /**
  28   * Advanced PHPUnit test case customised for Moodle.
  29   *
  30   * @package    core
  31   * @category   phpunit
  32   * @copyright  2012 Petr Skoda {@link http://skodak.org}
  33   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  abstract class advanced_testcase extends base_testcase {
  36      /** @var bool automatically reset everything? null means log changes */
  37      private $resetAfterTest;
  38  
  39      /** @var moodle_transaction */
  40      private $testdbtransaction;
  41  
  42      /** @var int timestamp used for current time asserts */
  43      private $currenttimestart;
  44  
  45      /**
  46       * Constructs a test case with the given name.
  47       *
  48       * Note: use setUp() or setUpBeforeClass() in your test cases.
  49       *
  50       * @param string $name
  51       * @param array  $data
  52       * @param string $dataName
  53       */
  54      final public function __construct($name = null, array $data = array(), $dataName = '') {
  55          parent::__construct($name, $data, $dataName);
  56  
  57          $this->setBackupGlobals(false);
  58          $this->setBackupStaticAttributes(false);
  59          $this->setPreserveGlobalState(false);
  60  
  61      }
  62  
  63      /**
  64       * Runs the bare test sequence.
  65       * @return void
  66       */
  67      final public function runBare(): void {
  68          global $DB;
  69  
  70          if (phpunit_util::$lastdbwrites != $DB->perf_get_writes()) {
  71              // this happens when previous test does not reset, we can not use transactions
  72              $this->testdbtransaction = null;
  73  
  74          } else if ($DB->get_dbfamily() === 'postgres' or $DB->get_dbfamily() === 'mssql') {
  75              // database must allow rollback of DDL, so no mysql here
  76              $this->testdbtransaction = $DB->start_delegated_transaction();
  77          }
  78  
  79          try {
  80              $this->setCurrentTimeStart();
  81              parent::runBare();
  82              // set DB reference in case somebody mocked it in test
  83              $DB = phpunit_util::get_global_backup('DB');
  84  
  85              // Deal with any debugging messages.
  86              $debugerror = phpunit_util::display_debugging_messages(true);
  87              $this->resetDebugging();
  88              if (!empty($debugerror)) {
  89                  trigger_error('Unexpected debugging() call detected.'."\n".$debugerror, E_USER_NOTICE);
  90              }
  91  
  92          } catch (Exception $ex) {
  93              $e = $ex;
  94          } catch (Throwable $ex) {
  95              // Engine errors in PHP7 throw exceptions of type Throwable (this "catch" will be ignored in PHP5).
  96              $e = $ex;
  97          }
  98  
  99          if (isset($e)) {
 100              // cleanup after failed expectation
 101              self::resetAllData();
 102              throw $e;
 103          }
 104  
 105          if (!$this->testdbtransaction or $this->testdbtransaction->is_disposed()) {
 106              $this->testdbtransaction = null;
 107          }
 108  
 109          if ($this->resetAfterTest === true) {
 110              if ($this->testdbtransaction) {
 111                  $DB->force_transaction_rollback();
 112                  phpunit_util::reset_all_database_sequences();
 113                  phpunit_util::$lastdbwrites = $DB->perf_get_writes(); // no db reset necessary
 114              }
 115              self::resetAllData(null);
 116  
 117          } else if ($this->resetAfterTest === false) {
 118              if ($this->testdbtransaction) {
 119                  $this->testdbtransaction->allow_commit();
 120              }
 121              // keep all data untouched for other tests
 122  
 123          } else {
 124              // reset but log what changed
 125              if ($this->testdbtransaction) {
 126                  try {
 127                      $this->testdbtransaction->allow_commit();
 128                  } catch (dml_transaction_exception $e) {
 129                      self::resetAllData();
 130                      throw new coding_exception('Invalid transaction state detected in test '.$this->getName());
 131                  }
 132              }
 133              self::resetAllData(true);
 134          }
 135  
 136          // Reset context cache.
 137          context_helper::reset_caches();
 138  
 139          // make sure test did not forget to close transaction
 140          if ($DB->is_transaction_started()) {
 141              self::resetAllData();
 142              if ($this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_PASSED
 143                  or $this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_SKIPPED
 144                  or $this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_INCOMPLETE) {
 145                  throw new coding_exception('Test '.$this->getName().' did not close database transaction');
 146              }
 147          }
 148      }
 149  
 150      /**
 151       * @deprecated since Moodle 3.10 - See MDL-67673 and MDL-64600 for more info.
 152       */
 153      protected function createXMLDataSet() {
 154          throw new coding_exception(__FUNCTION__ . '() is deprecated. Please use dataset_from_files() instead.');
 155      }
 156  
 157      /**
 158       * @deprecated since Moodle 3.10 - See MDL-67673 and MDL-64600 for more info.
 159       */
 160      protected function createCsvDataSet() {
 161          throw new coding_exception(__FUNCTION__ . '() is deprecated. Please use dataset_from_files() instead.');
 162      }
 163  
 164      /**
 165       * @deprecated since Moodle 3.10 - See MDL-67673 and MDL-64600 for more info.
 166       */
 167      protected function createArrayDataSet() {
 168          throw new coding_exception(__FUNCTION__ . '() is deprecated. Please use dataset_from_array() instead.');
 169      }
 170  
 171      /**
 172       * @deprecated since Moodle 3.10 - See MDL-67673 and MDL-64600 for more info.
 173       */
 174      protected function loadDataSet() {
 175          throw new coding_exception(__FUNCTION__ . '() is deprecated. Please use dataset->to_database() instead.');
 176      }
 177  
 178      /**
 179       * Creates a new dataset from CVS/XML files.
 180       *
 181       * This method accepts an array of full paths to CSV or XML files to be loaded
 182       * into the dataset. For CSV files, the name of the table which the file belongs
 183       * to needs to be specified. Example:
 184       *
 185       *   $fullpaths = [
 186       *       '/path/to/users.xml',
 187       *       'course' => '/path/to/courses.csv',
 188       *   ];
 189       *
 190       * @since Moodle 3.10
 191       *
 192       * @param array $files full paths to CSV or XML files to load.
 193       * @return phpunit_dataset
 194       */
 195      protected function dataset_from_files(array $files) {
 196          // We ignore $delimiter, $enclosure and $escape, use the default ones in your fixtures.
 197          $dataset = new phpunit_dataset();
 198          $dataset->from_files($files);
 199          return $dataset;
 200      }
 201  
 202      /**
 203       * Creates a new dataset from string (CSV or XML).
 204       *
 205       * @since Moodle 3.10
 206       *
 207       * @param string $content contents (CSV or XML) to load.
 208       * @param string $type format of the content to be loaded (csv or xml).
 209       * @param string $table name of the table which the file belongs to (only for CSV files).
 210       * @return phpunit_dataset
 211       */
 212      protected function dataset_from_string(string $content, string $type, ?string $table = null) {
 213          $dataset = new phpunit_dataset();
 214          $dataset->from_string($content, $type, $table);
 215          return $dataset;
 216      }
 217  
 218      /**
 219       * Creates a new dataset from PHP array.
 220       *
 221       * @since Moodle 3.10
 222       *
 223       * @param array $data array of tables, see {@see phpunit_dataset::from_array()} for supported formats.
 224       * @return phpunit_dataset
 225       */
 226      protected function dataset_from_array(array $data) {
 227          $dataset = new phpunit_dataset();
 228          $dataset->from_array($data);
 229          return $dataset;
 230      }
 231  
 232      /**
 233       * Call this method from test if you want to make sure that
 234       * the resetting of database is done the slow way without transaction
 235       * rollback.
 236       *
 237       * This is useful especially when testing stuff that is not compatible with transactions.
 238       *
 239       * @return void
 240       */
 241      public function preventResetByRollback() {
 242          if ($this->testdbtransaction and !$this->testdbtransaction->is_disposed()) {
 243              $this->testdbtransaction->allow_commit();
 244              $this->testdbtransaction = null;
 245          }
 246      }
 247  
 248      /**
 249       * Reset everything after current test.
 250       * @param bool $reset true means reset state back, false means keep all data for the next test,
 251       *      null means reset state and show warnings if anything changed
 252       * @return void
 253       */
 254      public function resetAfterTest($reset = true) {
 255          $this->resetAfterTest = $reset;
 256      }
 257  
 258      /**
 259       * Return debugging messages from the current test.
 260       * @return array with instances having 'message', 'level' and 'stacktrace' property.
 261       */
 262      public function getDebuggingMessages() {
 263          return phpunit_util::get_debugging_messages();
 264      }
 265  
 266      /**
 267       * Clear all previous debugging messages in current test
 268       * and revert to default DEVELOPER_DEBUG level.
 269       */
 270      public function resetDebugging() {
 271          phpunit_util::reset_debugging();
 272      }
 273  
 274      /**
 275       * Assert that exactly debugging was just called once.
 276       *
 277       * Discards the debugging message if successful.
 278       *
 279       * @param null|string $debugmessage null means any
 280       * @param null|string $debuglevel null means any
 281       * @param string $message
 282       */
 283      public function assertDebuggingCalled($debugmessage = null, $debuglevel = null, $message = '') {
 284          $debugging = $this->getDebuggingMessages();
 285          $debugdisplaymessage = "\n".phpunit_util::display_debugging_messages(true);
 286          $this->resetDebugging();
 287  
 288          $count = count($debugging);
 289  
 290          if ($count == 0) {
 291              if ($message === '') {
 292                  $message = 'Expectation failed, debugging() not triggered.';
 293              }
 294              $this->fail($message);
 295          }
 296          if ($count > 1) {
 297              if ($message === '') {
 298                  $message = 'Expectation failed, debugging() triggered '.$count.' times.'.$debugdisplaymessage;
 299              }
 300              $this->fail($message);
 301          }
 302          $this->assertEquals(1, $count);
 303  
 304          $message .= $debugdisplaymessage;
 305          $debug = reset($debugging);
 306          if ($debugmessage !== null) {
 307              $this->assertSame($debugmessage, $debug->message, $message);
 308          }
 309          if ($debuglevel !== null) {
 310              $this->assertSame($debuglevel, $debug->level, $message);
 311          }
 312      }
 313  
 314      /**
 315       * Asserts how many times debugging has been called.
 316       *
 317       * @param int $expectedcount The expected number of times
 318       * @param array $debugmessages Expected debugging messages, one for each expected message.
 319       * @param array $debuglevels Expected debugging levels, one for each expected message.
 320       * @param string $message
 321       * @return void
 322       */
 323      public function assertDebuggingCalledCount($expectedcount, $debugmessages = array(), $debuglevels = array(), $message = '') {
 324          if (!is_int($expectedcount)) {
 325              throw new coding_exception('assertDebuggingCalledCount $expectedcount argument should be an integer.');
 326          }
 327  
 328          $debugging = $this->getDebuggingMessages();
 329          $message .= "\n".phpunit_util::display_debugging_messages(true);
 330          $this->resetDebugging();
 331  
 332          $this->assertEquals($expectedcount, count($debugging), $message);
 333  
 334          if ($debugmessages) {
 335              if (!is_array($debugmessages) || count($debugmessages) != $expectedcount) {
 336                  throw new coding_exception('assertDebuggingCalledCount $debugmessages should contain ' . $expectedcount . ' messages');
 337              }
 338              foreach ($debugmessages as $key => $debugmessage) {
 339                  $this->assertSame($debugmessage, $debugging[$key]->message, $message);
 340              }
 341          }
 342  
 343          if ($debuglevels) {
 344              if (!is_array($debuglevels) || count($debuglevels) != $expectedcount) {
 345                  throw new coding_exception('assertDebuggingCalledCount $debuglevels should contain ' . $expectedcount . ' messages');
 346              }
 347              foreach ($debuglevels as $key => $debuglevel) {
 348                  $this->assertSame($debuglevel, $debugging[$key]->level, $message);
 349              }
 350          }
 351      }
 352  
 353      /**
 354       * Call when no debugging() messages expected.
 355       * @param string $message
 356       */
 357      public function assertDebuggingNotCalled($message = '') {
 358          $debugging = $this->getDebuggingMessages();
 359          $count = count($debugging);
 360  
 361          if ($message === '') {
 362              $message = 'Expectation failed, debugging() was triggered.';
 363          }
 364          $message .= "\n".phpunit_util::display_debugging_messages(true);
 365          $this->resetDebugging();
 366          $this->assertEquals(0, $count, $message);
 367      }
 368  
 369      /**
 370       * Assert that an event legacy data is equal to the expected value.
 371       *
 372       * @param mixed $expected expected data.
 373       * @param \core\event\base $event the event object.
 374       * @param string $message
 375       * @return void
 376       */
 377      public function assertEventLegacyData($expected, \core\event\base $event, $message = '') {
 378          $legacydata = phpunit_event_mock::testable_get_legacy_eventdata($event);
 379          if ($message === '') {
 380              $message = 'Event legacy data does not match expected value.';
 381          }
 382          $this->assertEquals($expected, $legacydata, $message);
 383      }
 384  
 385      /**
 386       * Assert that an event legacy log data is equal to the expected value.
 387       *
 388       * @param mixed $expected expected data.
 389       * @param \core\event\base $event the event object.
 390       * @param string $message
 391       * @return void
 392       */
 393      public function assertEventLegacyLogData($expected, \core\event\base $event, $message = '') {
 394          $legacydata = phpunit_event_mock::testable_get_legacy_logdata($event);
 395          if ($message === '') {
 396              $message = 'Event legacy log data does not match expected value.';
 397          }
 398          $this->assertEquals($expected, $legacydata, $message);
 399      }
 400  
 401      /**
 402       * Assert that an event is not using event->contxet.
 403       * While restoring context might not be valid and it should not be used by event url
 404       * or description methods.
 405       *
 406       * @param \core\event\base $event the event object.
 407       * @param string $message
 408       * @return void
 409       */
 410      public function assertEventContextNotUsed(\core\event\base $event, $message = '') {
 411          // Save current event->context and set it to false.
 412          $eventcontext = phpunit_event_mock::testable_get_event_context($event);
 413          phpunit_event_mock::testable_set_event_context($event, false);
 414          if ($message === '') {
 415              $message = 'Event should not use context property of event in any method.';
 416          }
 417  
 418          // Test event methods should not use event->context.
 419          $event->get_url();
 420          $event->get_description();
 421  
 422          // Restore event->context.
 423          phpunit_event_mock::testable_set_event_context($event, $eventcontext);
 424      }
 425  
 426      /**
 427       * Stores current time as the base for assertTimeCurrent().
 428       *
 429       * Note: this is called automatically before calling individual test methods.
 430       * @return int current time
 431       */
 432      public function setCurrentTimeStart() {
 433          $this->currenttimestart = time();
 434          return $this->currenttimestart;
 435      }
 436  
 437      /**
 438       * Assert that: start < $time < time()
 439       * @param int $time
 440       * @param string $message
 441       * @return void
 442       */
 443      public function assertTimeCurrent($time, $message = '') {
 444          $msg =  ($message === '') ? 'Time is lower that allowed start value' : $message;
 445          $this->assertGreaterThanOrEqual($this->currenttimestart, $time, $msg);
 446          $msg =  ($message === '') ? 'Time is in the future' : $message;
 447          $this->assertLessThanOrEqual(time(), $time, $msg);
 448      }
 449  
 450      /**
 451       * Starts message redirection.
 452       *
 453       * You can verify if messages were sent or not by inspecting the messages
 454       * array in the returned messaging sink instance. The redirection
 455       * can be stopped by calling $sink->close();
 456       *
 457       * @return phpunit_message_sink
 458       */
 459      public function redirectMessages() {
 460          return phpunit_util::start_message_redirection();
 461      }
 462  
 463      /**
 464       * Starts email redirection.
 465       *
 466       * You can verify if email were sent or not by inspecting the email
 467       * array in the returned phpmailer sink instance. The redirection
 468       * can be stopped by calling $sink->close();
 469       *
 470       * @return phpunit_message_sink
 471       */
 472      public function redirectEmails() {
 473          return phpunit_util::start_phpmailer_redirection();
 474      }
 475  
 476      /**
 477       * Starts event redirection.
 478       *
 479       * You can verify if events were triggered or not by inspecting the events
 480       * array in the returned event sink instance. The redirection
 481       * can be stopped by calling $sink->close();
 482       *
 483       * @return phpunit_event_sink
 484       */
 485      public function redirectEvents() {
 486          return phpunit_util::start_event_redirection();
 487      }
 488  
 489      /**
 490       * Override hook callbacks.
 491       *
 492       * @param string $hookname
 493       * @param callable $callback
 494       * @return void
 495       */
 496      public function redirectHook(string $hookname, callable $callback): void {
 497          \core\hook\manager::get_instance()->phpunit_redirect_hook($hookname, $callback);
 498      }
 499  
 500      /**
 501       * Remove all hook overrides.
 502       *
 503       * @return void
 504       */
 505      public function stopHookRedirections(): void {
 506          \core\hook\manager::get_instance()->phpunit_stop_redirections();
 507      }
 508  
 509      /**
 510       * Reset all database tables, restore global state and clear caches and optionally purge dataroot dir.
 511       *
 512       * @param bool $detectchanges
 513       *      true  - changes in global state and database are reported as errors
 514       *      false - no errors reported
 515       *      null  - only critical problems are reported as errors
 516       * @return void
 517       */
 518      public static function resetAllData($detectchanges = false) {
 519          phpunit_util::reset_all_data($detectchanges);
 520      }
 521  
 522      /**
 523       * Set current $USER, reset access cache.
 524       * @static
 525       * @param null|int|stdClass $user user record, null or 0 means non-logged-in, positive integer means userid
 526       * @return void
 527       */
 528      public static function setUser($user = null) {
 529          global $CFG, $DB;
 530  
 531          if (is_object($user)) {
 532              $user = clone($user);
 533          } else if (!$user) {
 534              $user = new stdClass();
 535              $user->id = 0;
 536              $user->mnethostid = $CFG->mnet_localhost_id;
 537          } else {
 538              $user = $DB->get_record('user', array('id'=>$user));
 539          }
 540          unset($user->description);
 541          unset($user->access);
 542          unset($user->preference);
 543  
 544          // Enusre session is empty, as it may contain caches and user specific info.
 545          \core\session\manager::init_empty_session();
 546  
 547          \core\session\manager::set_user($user);
 548      }
 549  
 550      /**
 551       * Set current $USER to admin account, reset access cache.
 552       * @static
 553       * @return void
 554       */
 555      public static function setAdminUser() {
 556          self::setUser(2);
 557      }
 558  
 559      /**
 560       * Set current $USER to guest account, reset access cache.
 561       * @static
 562       * @return void
 563       */
 564      public static function setGuestUser() {
 565          self::setUser(1);
 566      }
 567  
 568      /**
 569       * Change server and default php timezones.
 570       *
 571       * @param string $servertimezone timezone to set in $CFG->timezone (not validated)
 572       * @param string $defaultphptimezone timezone to fake default php timezone (must be valid)
 573       */
 574      public static function setTimezone($servertimezone = 'Australia/Perth', $defaultphptimezone = 'Australia/Perth') {
 575          global $CFG;
 576          $CFG->timezone = $servertimezone;
 577          core_date::phpunit_override_default_php_timezone($defaultphptimezone);
 578          core_date::set_default_server_timezone();
 579      }
 580  
 581      /**
 582       * Get data generator
 583       * @static
 584       * @return testing_data_generator
 585       */
 586      public static function getDataGenerator() {
 587          return phpunit_util::get_data_generator();
 588      }
 589  
 590      /**
 591       * Returns UTL of the external test file.
 592       *
 593       * The result depends on the value of following constants:
 594       *  - TEST_EXTERNAL_FILES_HTTP_URL
 595       *  - TEST_EXTERNAL_FILES_HTTPS_URL
 596       *
 597       * They should point to standard external test files repository,
 598       * it defaults to 'http://download.moodle.org/unittest'.
 599       *
 600       * False value means skip tests that require external files.
 601       *
 602       * @param string $path
 603       * @param bool $https true if https required
 604       * @return string url
 605       */
 606      public function getExternalTestFileUrl($path, $https = false) {
 607          $path = ltrim($path, '/');
 608          if ($path) {
 609              $path = '/'.$path;
 610          }
 611          if ($https) {
 612              if (defined('TEST_EXTERNAL_FILES_HTTPS_URL')) {
 613                  if (!TEST_EXTERNAL_FILES_HTTPS_URL) {
 614                      $this->markTestSkipped('Tests using external https test files are disabled');
 615                  }
 616                  return TEST_EXTERNAL_FILES_HTTPS_URL.$path;
 617              }
 618              return 'https://download.moodle.org/unittest'.$path;
 619          }
 620  
 621          if (defined('TEST_EXTERNAL_FILES_HTTP_URL')) {
 622              if (!TEST_EXTERNAL_FILES_HTTP_URL) {
 623                  $this->markTestSkipped('Tests using external http test files are disabled');
 624              }
 625              return TEST_EXTERNAL_FILES_HTTP_URL.$path;
 626          }
 627          return 'http://download.moodle.org/unittest'.$path;
 628      }
 629  
 630      /**
 631       * Recursively visit all the files in the source tree. Calls the callback
 632       * function with the pathname of each file found.
 633       *
 634       * @param string $path the folder to start searching from.
 635       * @param string $callback the method of this class to call with the name of each file found.
 636       * @param string $fileregexp a regexp used to filter the search (optional).
 637       * @param bool $exclude If true, pathnames that match the regexp will be ignored. If false,
 638       *     only files that match the regexp will be included. (default false).
 639       * @param array $ignorefolders will not go into any of these folders (optional).
 640       * @return void
 641       */
 642      public function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
 643          $files = scandir($path);
 644  
 645          foreach ($files as $file) {
 646              $filepath = $path .'/'. $file;
 647              if (strpos($file, '.') === 0) {
 648                  /// Don't check hidden files.
 649                  continue;
 650              } else if (is_dir($filepath)) {
 651                  if (!in_array($filepath, $ignorefolders)) {
 652                      $this->recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
 653                  }
 654              } else if ($exclude xor preg_match($fileregexp, $filepath)) {
 655                  $this->$callback($filepath);
 656              }
 657          }
 658      }
 659  
 660      /**
 661       * Wait for a second to roll over, ensures future calls to time() return a different result.
 662       *
 663       * This is implemented instead of sleep() as we do not need to wait a full second. In some cases
 664       * due to calls we may wait more than sleep() would have, on average it will be less.
 665       */
 666      public function waitForSecond() {
 667          $starttime = time();
 668          while (time() == $starttime) {
 669              usleep(50000);
 670          }
 671      }
 672  
 673      /**
 674       * Run adhoc tasks, optionally matching the specified classname.
 675       *
 676       * @param   string  $matchclass The name of the class to match on.
 677       * @param   int     $matchuserid The userid to match.
 678       */
 679      protected function runAdhocTasks($matchclass = '', $matchuserid = null) {
 680          global $DB;
 681  
 682          $params = [];
 683          if (!empty($matchclass)) {
 684              if (strpos($matchclass, '\\') !== 0) {
 685                  $matchclass = '\\' . $matchclass;
 686              }
 687              $params['classname'] = $matchclass;
 688          }
 689  
 690          if (!empty($matchuserid)) {
 691              $params['userid'] = $matchuserid;
 692          }
 693  
 694          $lock = $this->createMock(\core\lock\lock::class);
 695          $cronlock = $this->createMock(\core\lock\lock::class);
 696  
 697          $tasks = $DB->get_recordset('task_adhoc', $params);
 698          foreach ($tasks as $record) {
 699              // Note: This is for cron only.
 700              // We do not lock the tasks.
 701              $task = \core\task\manager::adhoc_task_from_record($record);
 702  
 703              $user = null;
 704              if ($userid = $task->get_userid()) {
 705                  // This task has a userid specified.
 706                  $user = \core_user::get_user($userid);
 707  
 708                  // User found. Check that they are suitable.
 709                  \core_user::require_active_user($user, true, true);
 710              }
 711  
 712              $task->set_lock($lock);
 713              if (!$task->is_blocking()) {
 714                  $cronlock->release();
 715              } else {
 716                  $task->set_cron_lock($cronlock);
 717              }
 718  
 719              \core\cron::prepare_core_renderer();
 720              \core\cron::setup_user($user);
 721  
 722              $task->execute();
 723              \core\task\manager::adhoc_task_complete($task);
 724  
 725              unset($task);
 726          }
 727          $tasks->close();
 728      }
 729  
 730      /**
 731       * Run adhoc tasks.
 732       */
 733      protected function run_all_adhoc_tasks(): void {
 734          // Run the adhoc task.
 735          while ($task = \core\task\manager::get_next_adhoc_task(time())) {
 736              $task->execute();
 737              \core\task\manager::adhoc_task_complete($task);
 738          }
 739      }
 740  }