Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [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       * Reset all database tables, restore global state and clear caches and optionally purge dataroot dir.
 491       *
 492       * @param bool $detectchanges
 493       *      true  - changes in global state and database are reported as errors
 494       *      false - no errors reported
 495       *      null  - only critical problems are reported as errors
 496       * @return void
 497       */
 498      public static function resetAllData($detectchanges = false) {
 499          phpunit_util::reset_all_data($detectchanges);
 500      }
 501  
 502      /**
 503       * Set current $USER, reset access cache.
 504       * @static
 505       * @param null|int|stdClass $user user record, null or 0 means non-logged-in, positive integer means userid
 506       * @return void
 507       */
 508      public static function setUser($user = null) {
 509          global $CFG, $DB;
 510  
 511          if (is_object($user)) {
 512              $user = clone($user);
 513          } else if (!$user) {
 514              $user = new stdClass();
 515              $user->id = 0;
 516              $user->mnethostid = $CFG->mnet_localhost_id;
 517          } else {
 518              $user = $DB->get_record('user', array('id'=>$user));
 519          }
 520          unset($user->description);
 521          unset($user->access);
 522          unset($user->preference);
 523  
 524          // Enusre session is empty, as it may contain caches and user specific info.
 525          \core\session\manager::init_empty_session();
 526  
 527          \core\session\manager::set_user($user);
 528      }
 529  
 530      /**
 531       * Set current $USER to admin account, reset access cache.
 532       * @static
 533       * @return void
 534       */
 535      public static function setAdminUser() {
 536          self::setUser(2);
 537      }
 538  
 539      /**
 540       * Set current $USER to guest account, reset access cache.
 541       * @static
 542       * @return void
 543       */
 544      public static function setGuestUser() {
 545          self::setUser(1);
 546      }
 547  
 548      /**
 549       * Change server and default php timezones.
 550       *
 551       * @param string $servertimezone timezone to set in $CFG->timezone (not validated)
 552       * @param string $defaultphptimezone timezone to fake default php timezone (must be valid)
 553       */
 554      public static function setTimezone($servertimezone = 'Australia/Perth', $defaultphptimezone = 'Australia/Perth') {
 555          global $CFG;
 556          $CFG->timezone = $servertimezone;
 557          core_date::phpunit_override_default_php_timezone($defaultphptimezone);
 558          core_date::set_default_server_timezone();
 559      }
 560  
 561      /**
 562       * Get data generator
 563       * @static
 564       * @return testing_data_generator
 565       */
 566      public static function getDataGenerator() {
 567          return phpunit_util::get_data_generator();
 568      }
 569  
 570      /**
 571       * Returns UTL of the external test file.
 572       *
 573       * The result depends on the value of following constants:
 574       *  - TEST_EXTERNAL_FILES_HTTP_URL
 575       *  - TEST_EXTERNAL_FILES_HTTPS_URL
 576       *
 577       * They should point to standard external test files repository,
 578       * it defaults to 'http://download.moodle.org/unittest'.
 579       *
 580       * False value means skip tests that require external files.
 581       *
 582       * @param string $path
 583       * @param bool $https true if https required
 584       * @return string url
 585       */
 586      public function getExternalTestFileUrl($path, $https = false) {
 587          $path = ltrim($path, '/');
 588          if ($path) {
 589              $path = '/'.$path;
 590          }
 591          if ($https) {
 592              if (defined('TEST_EXTERNAL_FILES_HTTPS_URL')) {
 593                  if (!TEST_EXTERNAL_FILES_HTTPS_URL) {
 594                      $this->markTestSkipped('Tests using external https test files are disabled');
 595                  }
 596                  return TEST_EXTERNAL_FILES_HTTPS_URL.$path;
 597              }
 598              return 'https://download.moodle.org/unittest'.$path;
 599          }
 600  
 601          if (defined('TEST_EXTERNAL_FILES_HTTP_URL')) {
 602              if (!TEST_EXTERNAL_FILES_HTTP_URL) {
 603                  $this->markTestSkipped('Tests using external http test files are disabled');
 604              }
 605              return TEST_EXTERNAL_FILES_HTTP_URL.$path;
 606          }
 607          return 'http://download.moodle.org/unittest'.$path;
 608      }
 609  
 610      /**
 611       * Recursively visit all the files in the source tree. Calls the callback
 612       * function with the pathname of each file found.
 613       *
 614       * @param string $path the folder to start searching from.
 615       * @param string $callback the method of this class to call with the name of each file found.
 616       * @param string $fileregexp a regexp used to filter the search (optional).
 617       * @param bool $exclude If true, pathnames that match the regexp will be ignored. If false,
 618       *     only files that match the regexp will be included. (default false).
 619       * @param array $ignorefolders will not go into any of these folders (optional).
 620       * @return void
 621       */
 622      public function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
 623          $files = scandir($path);
 624  
 625          foreach ($files as $file) {
 626              $filepath = $path .'/'. $file;
 627              if (strpos($file, '.') === 0) {
 628                  /// Don't check hidden files.
 629                  continue;
 630              } else if (is_dir($filepath)) {
 631                  if (!in_array($filepath, $ignorefolders)) {
 632                      $this->recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
 633                  }
 634              } else if ($exclude xor preg_match($fileregexp, $filepath)) {
 635                  $this->$callback($filepath);
 636              }
 637          }
 638      }
 639  
 640      /**
 641       * Wait for a second to roll over, ensures future calls to time() return a different result.
 642       *
 643       * This is implemented instead of sleep() as we do not need to wait a full second. In some cases
 644       * due to calls we may wait more than sleep() would have, on average it will be less.
 645       */
 646      public function waitForSecond() {
 647          $starttime = time();
 648          while (time() == $starttime) {
 649              usleep(50000);
 650          }
 651      }
 652  
 653      /**
 654       * Run adhoc tasks, optionally matching the specified classname.
 655       *
 656       * @param   string  $matchclass The name of the class to match on.
 657       * @param   int     $matchuserid The userid to match.
 658       */
 659      protected function runAdhocTasks($matchclass = '', $matchuserid = null) {
 660          global $DB;
 661  
 662          $params = [];
 663          if (!empty($matchclass)) {
 664              if (strpos($matchclass, '\\') !== 0) {
 665                  $matchclass = '\\' . $matchclass;
 666              }
 667              $params['classname'] = $matchclass;
 668          }
 669  
 670          if (!empty($matchuserid)) {
 671              $params['userid'] = $matchuserid;
 672          }
 673  
 674          $lock = $this->createMock(\core\lock\lock::class);
 675          $cronlock = $this->createMock(\core\lock\lock::class);
 676  
 677          $tasks = $DB->get_recordset('task_adhoc', $params);
 678          foreach ($tasks as $record) {
 679              // Note: This is for cron only.
 680              // We do not lock the tasks.
 681              $task = \core\task\manager::adhoc_task_from_record($record);
 682  
 683              $user = null;
 684              if ($userid = $task->get_userid()) {
 685                  // This task has a userid specified.
 686                  $user = \core_user::get_user($userid);
 687  
 688                  // User found. Check that they are suitable.
 689                  \core_user::require_active_user($user, true, true);
 690              }
 691  
 692              $task->set_lock($lock);
 693              if (!$task->is_blocking()) {
 694                  $cronlock->release();
 695              } else {
 696                  $task->set_cron_lock($cronlock);
 697              }
 698  
 699              \core\cron::prepare_core_renderer();
 700              \core\cron::setup_user($user);
 701  
 702              $task->execute();
 703              \core\task\manager::adhoc_task_complete($task);
 704  
 705              unset($task);
 706          }
 707          $tasks->close();
 708      }
 709  }