Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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